feat(web): improve websocket filtering and add restored assets support
- Refactor websocket support to use modular filter functions - Add support for on_asset_restore events - Improve handling of asset updates with proper filtering for visibility, favorites, trash, tags, albums, and persons - Add null checks in timeline manager for empty arrays
This commit is contained in:
@@ -7,27 +7,84 @@ import type { Unsubscriber } from 'svelte/store';
|
|||||||
|
|
||||||
const PROCESS_DELAY_MS = 2500;
|
const PROCESS_DELAY_MS = 2500;
|
||||||
|
|
||||||
|
const fetchAssetInfos = async (assetIds: string[]) => {
|
||||||
|
return await Promise.all(assetIds.map((id) => getAssetInfo({ id, key: authManager.key })));
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetFilter = (
|
||||||
|
asset: Awaited<ReturnType<typeof getAssetInfo>>,
|
||||||
|
timelineManager: TimelineManager,
|
||||||
|
) => Promise<boolean> | boolean;
|
||||||
|
|
||||||
|
// Filter functions
|
||||||
|
const checkVisibilityProperty: AssetFilter = (asset, timelineManager) => {
|
||||||
|
const timelineAsset = toTimelineAsset(asset);
|
||||||
|
return (
|
||||||
|
timelineManager.options.visibility === undefined || timelineManager.options.visibility === timelineAsset.visibility
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkFavoriteProperty: AssetFilter = (asset, timelineManager) => {
|
||||||
|
const timelineAsset = toTimelineAsset(asset);
|
||||||
|
return (
|
||||||
|
timelineManager.options.isFavorite === undefined || timelineManager.options.isFavorite === timelineAsset.isFavorite
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTrashedProperty: AssetFilter = (asset, timelineManager) => {
|
||||||
|
const timelineAsset = toTimelineAsset(asset);
|
||||||
|
return (
|
||||||
|
timelineManager.options.isTrashed === undefined || timelineManager.options.isTrashed === timelineAsset.isTrashed
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTagProperty: AssetFilter = (asset, timelineManager) => {
|
||||||
|
if (!timelineManager.options.tagId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasMatchingTag = asset.tags?.some((tag: { id: string }) => tag.id === timelineManager.options.tagId);
|
||||||
|
return !!hasMatchingTag;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAlbumProperty: AssetFilter = async (asset, timelineManager) => {
|
||||||
|
if (!timelineManager.options.albumId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const albums = await getAllAlbums({ assetId: asset.id });
|
||||||
|
return albums.some((album) => album.id === timelineManager.options.albumId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPersonProperty: AssetFilter = (asset, timelineManager) => {
|
||||||
|
if (!timelineManager.options.personId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasMatchingPerson = asset.people?.some(
|
||||||
|
(person: { id: string }) => person.id === timelineManager.options.personId,
|
||||||
|
);
|
||||||
|
return !!hasMatchingPerson;
|
||||||
|
};
|
||||||
|
|
||||||
export class WebsocketSupport {
|
export class WebsocketSupport {
|
||||||
readonly #timelineManager: TimelineManager;
|
readonly #timelineManager: TimelineManager;
|
||||||
#unsubscribers: Unsubscriber[] = [];
|
#unsubscribers: Unsubscriber[] = [];
|
||||||
|
|
||||||
#pendingUpdates: {
|
#pendingUpdates: {
|
||||||
updated: AssetResponseDto[];
|
updated: string[];
|
||||||
trashed: string[];
|
trashed: string[];
|
||||||
|
restored: string[];
|
||||||
deleted: string[];
|
deleted: string[];
|
||||||
personed: { assetId: string; personId: string | undefined; status: 'created' | 'removed' | 'removed_soft' }[];
|
personed: { assetId: string; personId: string | undefined; status: 'created' | 'removed' | 'removed_soft' }[];
|
||||||
album: { albumId: string; assetId: string[]; status: 'added' | 'removed' }[];
|
album: { albumId: string; assetId: string[]; status: 'added' | 'removed' }[];
|
||||||
} = {
|
|
||||||
updated: [],
|
|
||||||
trashed: [],
|
|
||||||
deleted: [],
|
|
||||||
personed: [],
|
|
||||||
album: [],
|
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Count of pending updates across all categories.
|
||||||
|
* This is used to determine if there are any updates to process.
|
||||||
|
*/
|
||||||
#pendingCount() {
|
#pendingCount() {
|
||||||
return (
|
return (
|
||||||
this.#pendingUpdates.updated.length +
|
this.#pendingUpdates.updated.length +
|
||||||
this.#pendingUpdates.trashed.length +
|
this.#pendingUpdates.trashed.length +
|
||||||
|
this.#pendingUpdates.restored.length +
|
||||||
this.#pendingUpdates.deleted.length +
|
this.#pendingUpdates.deleted.length +
|
||||||
this.#pendingUpdates.personed.length +
|
this.#pendingUpdates.personed.length +
|
||||||
this.#pendingUpdates.album.length
|
this.#pendingUpdates.album.length
|
||||||
@@ -37,24 +94,38 @@ export class WebsocketSupport {
|
|||||||
#isProcessing = false;
|
#isProcessing = false;
|
||||||
|
|
||||||
constructor(timelineManager: TimelineManager) {
|
constructor(timelineManager: TimelineManager) {
|
||||||
|
this.#pendingUpdates = this.#init();
|
||||||
this.#timelineManager = timelineManager;
|
this.#timelineManager = timelineManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#init() {
|
||||||
|
return {
|
||||||
|
updated: [],
|
||||||
|
trashed: [],
|
||||||
|
restored: [],
|
||||||
|
deleted: [],
|
||||||
|
personed: [],
|
||||||
|
album: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
connectWebsocketEvents() {
|
connectWebsocketEvents() {
|
||||||
this.#unsubscribers.push(
|
this.#unsubscribers.push(
|
||||||
websocketEvents.on('on_asset_trash', (ids) => {
|
websocketEvents.on('on_asset_trash', (ids) => {
|
||||||
this.#pendingUpdates.trashed.push(...ids);
|
this.#pendingUpdates.trashed.push(...ids);
|
||||||
this.#scheduleProcessing();
|
this.#scheduleProcessing();
|
||||||
}),
|
}),
|
||||||
|
// this event is called when an person is added or removed from an asset
|
||||||
websocketEvents.on('on_asset_person', (data) => {
|
websocketEvents.on('on_asset_person', (data) => {
|
||||||
this.#pendingUpdates.personed.push(data);
|
this.#pendingUpdates.personed.push(data);
|
||||||
this.#scheduleProcessing();
|
this.#scheduleProcessing();
|
||||||
}),
|
}),
|
||||||
// uploads and tagging are handled by this event
|
// uploads and tagging are handled by this event
|
||||||
websocketEvents.on('on_asset_update', (asset) => {
|
websocketEvents.on('on_asset_update', (ids) => {
|
||||||
this.#pendingUpdates.updated.push(asset);
|
this.#pendingUpdates.updated.push(...ids);
|
||||||
this.#scheduleProcessing();
|
this.#scheduleProcessing();
|
||||||
}),
|
}),
|
||||||
|
// this event is called when an asseted is added or removed from an album
|
||||||
websocketEvents.on('on_album_update', (data) => {
|
websocketEvents.on('on_album_update', (data) => {
|
||||||
this.#pendingUpdates.album.push(data);
|
this.#pendingUpdates.album.push(data);
|
||||||
this.#scheduleProcessing();
|
this.#scheduleProcessing();
|
||||||
@@ -67,6 +138,10 @@ export class WebsocketSupport {
|
|||||||
this.#pendingUpdates.deleted.push(ids);
|
this.#pendingUpdates.deleted.push(ids);
|
||||||
this.#scheduleProcessing();
|
this.#scheduleProcessing();
|
||||||
}),
|
}),
|
||||||
|
websocketEvents.on('on_asset_restore', (ids) => {
|
||||||
|
this.#pendingUpdates.restored.push(...ids);
|
||||||
|
this.#scheduleProcessing();
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,100 +195,113 @@ export class WebsocketSupport {
|
|||||||
|
|
||||||
async #process() {
|
async #process() {
|
||||||
const pendingUpdates = this.#pendingUpdates;
|
const pendingUpdates = this.#pendingUpdates;
|
||||||
this.#pendingUpdates = {
|
this.#pendingUpdates = this.#init();
|
||||||
updated: [],
|
|
||||||
trashed: [],
|
await this.#handleGeneric(
|
||||||
deleted: [],
|
[...pendingUpdates.updated, ...pendingUpdates.trashed, ...pendingUpdates.restored],
|
||||||
personed: [],
|
[checkVisibilityProperty, checkFavoriteProperty, checkTrashedProperty, checkTagProperty, checkAlbumProperty],
|
||||||
album: [],
|
);
|
||||||
};
|
|
||||||
await this.#handleUpdatedAssets(pendingUpdates.updated);
|
|
||||||
await this.#handleUpdatedAssetsPerson(pendingUpdates.personed);
|
await this.#handleUpdatedAssetsPerson(pendingUpdates.personed);
|
||||||
await this.#handleUpdatedAssetsAlbum(pendingUpdates.album);
|
await this.#handleUpdatedAssetsAlbum(pendingUpdates.album);
|
||||||
await this.#handleUpdatedAssetsTrashed(pendingUpdates.trashed);
|
|
||||||
this.#timelineManager.removeAssets(pendingUpdates.deleted);
|
this.#timelineManager.removeAssets(pendingUpdates.deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleUpdatedAssets(assets: AssetResponseDto[]) {
|
async #handleGeneric(assetIds: string[], filters: AssetFilter[]) {
|
||||||
const prefilteredAssets = assets.filter((asset) => !this.#timelineManager.isExcluded(toTimelineAsset(asset)));
|
if (assetIds.length === 0) {
|
||||||
if (!this.#timelineManager.options.albumId) {
|
return;
|
||||||
// also check tags
|
}
|
||||||
if (!this.#timelineManager.options.tagId) {
|
|
||||||
return this.#timelineManager.addAssets(prefilteredAssets.map((asset) => toTimelineAsset(asset)));
|
const assets = await fetchAssetInfos(assetIds);
|
||||||
}
|
const assetsToAdd = [];
|
||||||
for (const asset of prefilteredAssets) {
|
const assetsToRemove = [];
|
||||||
if (asset.tags?.some((tag) => tag.id === this.#timelineManager.options.tagId)) {
|
|
||||||
this.#timelineManager.addAssets([toTimelineAsset(asset)]);
|
for (const asset of assets) {
|
||||||
} else {
|
if (await this.#shouldAssetBeIncluded(asset, filters)) {
|
||||||
this.#timelineManager.removeAssets([asset.id]);
|
assetsToAdd.push(asset);
|
||||||
}
|
} else {
|
||||||
|
assetsToRemove.push(asset.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const matchingAssets = [];
|
|
||||||
for (const asset of prefilteredAssets) {
|
this.#timelineManager.addAssets(assetsToAdd.map((asset) => toTimelineAsset(asset)));
|
||||||
const albums = await getAllAlbums({ assetId: asset.id });
|
this.#timelineManager.removeAssets(assetsToRemove);
|
||||||
if (albums.some((album) => album.id === this.#timelineManager.options.albumId)) {
|
}
|
||||||
if (this.#timelineManager.options.tagId) {
|
|
||||||
if (asset.tags?.some((tag) => tag.id === this.#timelineManager.options.tagId)) {
|
async #shouldAssetBeIncluded(asset: AssetResponseDto, filters: AssetFilter[]): Promise<boolean> {
|
||||||
matchingAssets.push(asset);
|
for (const filter of filters) {
|
||||||
} else {
|
const result = await filter(asset, this.#timelineManager);
|
||||||
this.#timelineManager.removeAssets([asset.id]);
|
if (!result) {
|
||||||
}
|
return false;
|
||||||
} else {
|
|
||||||
matchingAssets.push(asset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.#timelineManager.addAssets(matchingAssets.map((asset) => toTimelineAsset(asset)));
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleUpdatedAssetsPerson(
|
async #handleUpdatedAssetsPerson(
|
||||||
data: { assetId: string; personId: string | undefined; status: 'created' | 'removed' | 'removed_soft' }[],
|
data: { assetId: string; personId: string | undefined; status: 'created' | 'removed' | 'removed_soft' }[],
|
||||||
) {
|
) {
|
||||||
if (!this.#timelineManager.options.personId) {
|
const assetsToRemove: string[] = [];
|
||||||
for (const { assetId } of data) {
|
const personAssetsToAdd: string[] = [];
|
||||||
const asset = await getAssetInfo({ id: assetId, key: authManager.key });
|
|
||||||
this.#timelineManager.addAssets([toTimelineAsset(asset)]);
|
if (this.#timelineManager.options.personId === undefined) {
|
||||||
|
// If no person filter, we just add all assets with a person change
|
||||||
|
personAssetsToAdd.push(...data.map((d) => d.assetId));
|
||||||
|
} else {
|
||||||
|
for (const { assetId, personId, status } of data) {
|
||||||
|
if (status === 'created' && personId === this.#timelineManager.options.personId) {
|
||||||
|
personAssetsToAdd.push(assetId);
|
||||||
|
} else if (
|
||||||
|
(status === 'removed' || status === 'removed_soft') &&
|
||||||
|
personId === this.#timelineManager.options.personId
|
||||||
|
) {
|
||||||
|
assetsToRemove.push(assetId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
for (const { assetId, personId, status } of data) {
|
|
||||||
if (status === 'created') {
|
this.#timelineManager.removeAssets(assetsToRemove);
|
||||||
if (personId !== this.#timelineManager.options.personId) {
|
// At this point, personAssetsToAdd contains assets that now have the target person,
|
||||||
|
// but we need to check if they still match other filters.
|
||||||
|
await this.#handleGeneric(personAssetsToAdd, [
|
||||||
|
checkVisibilityProperty,
|
||||||
|
checkFavoriteProperty,
|
||||||
|
checkTrashedProperty,
|
||||||
|
checkTagProperty,
|
||||||
|
checkAlbumProperty,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #handleUpdatedAssetsAlbum(data: { albumId: string; assetId: string[]; status: 'added' | 'removed' }[]) {
|
||||||
|
const assetsToAdd: string[] = [];
|
||||||
|
const assetsToRemove: string[] = [];
|
||||||
|
|
||||||
|
if (this.#timelineManager.options.albumId === undefined) {
|
||||||
|
// If no album filter, we just add all assets with an album change
|
||||||
|
assetsToAdd.push(...data.flatMap((d) => d.assetId));
|
||||||
|
} else {
|
||||||
|
for (const { albumId, assetId, status } of data) {
|
||||||
|
if (albumId !== this.#timelineManager.options.albumId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const asset = await getAssetInfo({ id: assetId, key: authManager.key });
|
if (status === 'added') {
|
||||||
this.#timelineManager.addAssets([toTimelineAsset(asset)]);
|
assetsToAdd.push(...assetId);
|
||||||
} else if (personId === this.#timelineManager.options.personId) {
|
} else if (status === 'removed') {
|
||||||
this.#timelineManager.removeAssets([assetId]);
|
assetsToRemove.push(...assetId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
async #handleUpdatedAssetsAlbum(data: { albumId: string; assetId: string[]; status: 'added' | 'removed' }[]) {
|
this.#timelineManager.removeAssets(assetsToRemove);
|
||||||
if (!this.#timelineManager.options.albumId) {
|
// At this point, assetsToAdd contains assets that now have the target person,
|
||||||
return;
|
// but we need to check if they still match other filters.
|
||||||
}
|
await this.#handleGeneric(assetsToAdd, [
|
||||||
for (const { albumId, assetId, status } of data) {
|
checkVisibilityProperty,
|
||||||
if (albumId !== this.#timelineManager.options.albumId) {
|
checkFavoriteProperty,
|
||||||
continue;
|
checkTrashedProperty,
|
||||||
}
|
checkTagProperty,
|
||||||
if (status === 'added') {
|
checkPersonProperty,
|
||||||
const assets = await Promise.all(assetId.map((id) => getAssetInfo({ id, key: authManager.key })));
|
]);
|
||||||
this.#timelineManager.addAssets(assets.map((element) => toTimelineAsset(element)));
|
|
||||||
} else if (status === 'removed') {
|
|
||||||
this.#timelineManager.removeAssets(assetId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async #handleUpdatedAssetsTrashed(trashed: string[]) {
|
|
||||||
if (this.#timelineManager.options.isTrashed === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.#timelineManager.options.isTrashed) {
|
|
||||||
const assets = await Promise.all(trashed.map((id) => getAssetInfo({ id, key: authManager.key })));
|
|
||||||
this.#timelineManager.addAssets(assets.map((element) => toTimelineAsset(element)));
|
|
||||||
} else {
|
|
||||||
this.#timelineManager.removeAssets(trashed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -411,6 +411,9 @@ export class TimelineManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addAssets(assets: TimelineAsset[]) {
|
addAssets(assets: TimelineAsset[]) {
|
||||||
|
if (assets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset));
|
const assetsToUpdate = assets.filter((asset) => !this.isExcluded(asset));
|
||||||
const notUpdated = this.updateAssets(assetsToUpdate);
|
const notUpdated = this.updateAssets(assetsToUpdate);
|
||||||
addAssetsToMonthGroups(this, [...notUpdated], { order: this.#options.order ?? AssetOrder.Desc });
|
addAssetsToMonthGroups(this, [...notUpdated], { order: this.#options.order ?? AssetOrder.Desc });
|
||||||
@@ -475,6 +478,9 @@ export class TimelineManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeAssets(ids: string[]) {
|
removeAssets(ids: string[]) {
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const { unprocessedIds } = runAssetOperation(
|
const { unprocessedIds } = runAssetOperation(
|
||||||
this,
|
this,
|
||||||
new Set(ids),
|
new Set(ids),
|
||||||
|
|||||||
Reference in New Issue
Block a user