Files
Toju/toju-app/src/app/domains/attachment/application/services/attachment-manager.service.ts
2026-05-18 19:38:08 +02:00

247 lines
7.5 KiB
TypeScript

import {
Injectable,
effect,
inject
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { ROOM_URL_PATTERN } from '../../../../core/constants';
import { shouldAutoRequestWhenWatched } from '../../domain/logic/attachment.logic';
import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.model';
import type {
FileAnnouncePayload,
FileCancelPayload,
FileChunkPayload,
FileNotFoundPayload,
FileRequestPayload
} from '../../domain/models/attachment-transfer.model';
import { AttachmentPersistenceService } from './attachment-persistence.service';
import { AttachmentRuntimeStore } from './attachment-runtime.store';
import { AttachmentTransferService } from './attachment-transfer.service';
@Injectable({ providedIn: 'root' })
export class AttachmentManagerService {
get updated() {
return this.runtimeStore.updated;
}
private readonly webrtc = inject(RealtimeSessionFacade);
private readonly router = inject(Router);
private readonly database = inject(DatabaseService);
private readonly runtimeStore = inject(AttachmentRuntimeStore);
private readonly persistence = inject(AttachmentPersistenceService);
private readonly transfer = inject(AttachmentTransferService);
private watchedRoomId: string | null = this.extractWatchedRoomId(this.router.url);
private isDatabaseInitialised = false;
private autoDownloadRequestsByRoom = new Map<string, Promise<void>>();
constructor() {
effect(() => {
if (this.database.isReady() && !this.isDatabaseInitialised) {
this.isDatabaseInitialised = true;
void this.persistence.initFromDatabase();
}
});
this.router.events.subscribe((event) => {
if (!(event instanceof NavigationEnd)) {
return;
}
this.watchedRoomId = this.extractWatchedRoomId(event.urlAfterRedirects || event.url);
if (this.watchedRoomId) {
void this.requestAutoDownloadsForRoom(this.watchedRoomId);
}
});
this.webrtc.onPeerConnected.subscribe(() => {
if (this.watchedRoomId) {
void this.requestAutoDownloadsForRoom(this.watchedRoomId);
}
});
}
getForMessage(messageId: string): Attachment[] {
return this.runtimeStore.getAttachmentsForMessage(messageId);
}
rememberMessageRoom(messageId: string, roomId: string): void {
if (!messageId || !roomId)
return;
this.runtimeStore.rememberMessageRoom(messageId, roomId);
}
queueAutoDownloadsForMessage(messageId: string, attachmentId?: string): void {
void this.requestAutoDownloadsForMessage(messageId, attachmentId);
}
async requestAutoDownloadsForRoom(roomId: string): Promise<void> {
if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0)
return;
const activeRequest = this.autoDownloadRequestsByRoom.get(roomId);
if (activeRequest) {
return activeRequest;
}
const request = this.runAutoDownloadsForRoom(roomId).finally(() => {
if (this.autoDownloadRequestsByRoom.get(roomId) === request) {
this.autoDownloadRequestsByRoom.delete(roomId);
}
});
this.autoDownloadRequestsByRoom.set(roomId, request);
return request;
}
async deleteForMessage(messageId: string): Promise<void> {
await this.persistence.deleteForMessage(messageId);
}
getAttachmentMetasForMessages(messageIds: string[]): Record<string, AttachmentMeta[]> {
return this.transfer.getAttachmentMetasForMessages(messageIds);
}
registerSyncedAttachments(
attachmentMap: Record<string, AttachmentMeta[]>,
messageRoomIds?: Record<string, string>
): void {
this.transfer.registerSyncedAttachments(attachmentMap, messageRoomIds);
for (const [messageId, attachments] of Object.entries(attachmentMap)) {
for (const attachment of attachments) {
this.queueAutoDownloadsForMessage(messageId, attachment.id);
}
}
}
requestFromAnyPeer(messageId: string, attachment: Attachment): void {
this.transfer.requestFromAnyPeer(messageId, attachment);
}
handleFileNotFound(payload: FileNotFoundPayload): void {
this.transfer.handleFileNotFound(payload);
}
requestImageFromAnyPeer(messageId: string, attachment: Attachment): void {
this.transfer.requestImageFromAnyPeer(messageId, attachment);
}
requestFile(messageId: string, attachment: Attachment): void {
this.transfer.requestFile(messageId, attachment);
}
async publishAttachments(
messageId: string,
files: File[],
uploaderPeerId?: string
): Promise<void> {
await this.transfer.publishAttachments(messageId, files, uploaderPeerId);
}
handleFileAnnounce(payload: FileAnnouncePayload): void {
this.transfer.handleFileAnnounce(payload);
if (payload.messageId && payload.file?.id) {
this.queueAutoDownloadsForMessage(payload.messageId, payload.file.id);
}
}
handleFileChunk(payload: FileChunkPayload): void {
this.transfer.handleFileChunk(payload);
}
async handleFileRequest(payload: FileRequestPayload): Promise<void> {
await this.transfer.handleFileRequest(payload);
}
cancelRequest(messageId: string, attachment: Attachment): void {
this.transfer.cancelRequest(messageId, attachment);
}
handleFileCancel(payload: FileCancelPayload): void {
this.transfer.handleFileCancel(payload);
}
async fulfillRequestWithFile(
messageId: string,
fileId: string,
targetPeerId: string,
file: File
): Promise<void> {
await this.transfer.fulfillRequestWithFile(messageId, fileId, targetPeerId, file);
}
private async runAutoDownloadsForRoom(roomId: string): Promise<void> {
if (!this.isRoomWatched(roomId)) {
return;
}
if (this.database.isReady()) {
const messages = await this.database.getMessages(roomId, 500, 0);
for (const message of messages) {
this.runtimeStore.rememberMessageRoom(message.id, message.roomId);
await this.requestAutoDownloadsForMessage(message.id);
}
return;
}
for (const [messageId] of this.runtimeStore.getAttachmentEntries()) {
const attachmentRoomId = await this.persistence.resolveMessageRoomId(messageId);
if (attachmentRoomId === roomId) {
await this.requestAutoDownloadsForMessage(messageId);
}
}
}
private async requestAutoDownloadsForMessage(messageId: string, attachmentId?: string): Promise<void> {
if (!messageId)
return;
const roomId = await this.persistence.resolveMessageRoomId(messageId);
if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0) {
return;
}
const attachments = this.runtimeStore.getAttachmentsForMessage(messageId);
for (const attachment of attachments) {
if (attachmentId && attachment.id !== attachmentId)
continue;
if (!shouldAutoRequestWhenWatched(attachment))
continue;
if (attachment.available)
continue;
if ((attachment.receivedBytes ?? 0) > 0)
continue;
if (this.transfer.hasPendingRequest(messageId, attachment.id))
continue;
this.transfer.requestFromAnyPeer(messageId, attachment);
}
}
private extractWatchedRoomId(url: string): string | null {
const roomMatch = url.match(ROOM_URL_PATTERN);
return roomMatch ? roomMatch[1] : null;
}
private isRoomWatched(roomId: string | null | undefined): boolean {
return !!roomId && roomId === this.watchedRoomId;
}
}