Fix private calls

This commit is contained in:
2026-05-17 15:14:52 +02:00
parent 0f6cb3ee77
commit e769a6ee4a
71 changed files with 5821 additions and 349 deletions

View File

@@ -28,6 +28,22 @@ import { AttachmentPersistenceService } from './attachment-persistence.service';
import { AttachmentRuntimeStore } from './attachment-runtime.store';
import { AttachmentTransferTransportService } from './attachment-transfer-transport.service';
interface DiskReceiveAssembly {
path: string;
receivedCount: number;
receivedIndexes: Set<number>;
total: number;
}
interface ValidFileChunkPayload {
data: string;
fileId: string;
fromPeerId?: string;
index: number;
messageId: string;
total: number;
}
@Injectable({ providedIn: 'root' })
export class AttachmentTransferService {
private readonly webrtc = inject(RealtimeSessionFacade);
@@ -36,6 +52,9 @@ export class AttachmentTransferService {
private readonly persistence = inject(AttachmentPersistenceService);
private readonly transport = inject(AttachmentTransferTransportService);
private readonly diskReceiveAssemblies = new Map<string, DiskReceiveAssembly>();
private readonly diskReceiveChains = new Map<string, Promise<void>>();
getAttachmentMetasForMessages(messageIds: string[]): Record<string, AttachmentMeta[]> {
const result: Record<string, AttachmentMeta[]> = {};
@@ -174,10 +193,19 @@ export class AttachmentTransferService {
attachments.push(attachment);
this.runtimeStore.setOriginalFile(`${messageId}:${fileId}`, file);
try {
attachment.objectUrl = URL.createObjectURL(file);
const fileUrl = attachment.filePath && this.isPlayableMedia(attachment)
? await this.attachmentStorage.getFileUrl(attachment.filePath)
: null;
if (fileUrl) {
attachment.objectUrl = fileUrl;
attachment.available = true;
} catch { /* non-critical */ }
} else {
try {
attachment.objectUrl = URL.createObjectURL(file);
attachment.available = true;
} catch { /* non-critical */ }
}
if (attachment.size <= MAX_AUTO_SAVE_SIZE_BYTES) {
void this.persistence.saveFileToDisk(attachment, file);
@@ -257,6 +285,19 @@ export class AttachmentTransferService {
if (!attachment)
return;
if (this.shouldReceiveToDisk(attachment)) {
this.enqueueDiskFileChunk(attachment, {
data,
fileId,
fromPeerId,
index,
messageId,
total
});
return;
}
const decodedBytes = this.transport.decodeBase64(data);
const assemblyKey = `${messageId}:${fileId}`;
const requestKey = this.buildRequestKey(messageId, fileId);
@@ -274,7 +315,7 @@ export class AttachmentTransferService {
this.updateTransferProgress(attachment, decodedBytes, fromPeerId);
this.runtimeStore.touch();
this.finalizeTransferIfComplete(attachment, assemblyKey, total);
void this.finalizeTransferIfComplete(attachment, assemblyKey, total);
}
async handleFileRequest(payload: FileRequestPayload): Promise<void> {
@@ -375,6 +416,7 @@ export class AttachmentTransferService {
this.runtimeStore.deleteChunkBuffer(assemblyKey);
this.runtimeStore.deleteChunkCount(assemblyKey);
void this.deleteDiskReceiveAssembly(assemblyKey);
attachment.receivedBytes = 0;
attachment.speedBps = 0;
@@ -533,11 +575,11 @@ export class AttachmentTransferService {
attachment.lastUpdateMs = now;
}
private finalizeTransferIfComplete(
private async finalizeTransferIfComplete(
attachment: Attachment,
assemblyKey: string,
total: number
): void {
): Promise<void> {
const receivedChunkCount = this.runtimeStore.getChunkCount(assemblyKey) ?? 0;
const completeBuffer = this.runtimeStore.getChunkBuffer(assemblyKey);
@@ -551,16 +593,167 @@ export class AttachmentTransferService {
const blob = new Blob(completeBuffer, { type: attachment.mime });
attachment.available = true;
attachment.objectUrl = URL.createObjectURL(blob);
if (shouldPersistDownloadedAttachment(attachment)) {
void this.persistence.saveFileToDisk(attachment, blob);
}
this.runtimeStore.deleteChunkBuffer(assemblyKey);
this.runtimeStore.deleteChunkCount(assemblyKey);
if (shouldPersistDownloadedAttachment(attachment)) {
const diskPath = await this.persistence.saveFileToDisk(attachment, blob);
const fileUrl = diskPath && this.isPlayableMedia(attachment)
? await this.attachmentStorage.getFileUrl(diskPath)
: null;
if (fileUrl) {
attachment.objectUrl = fileUrl;
} else {
attachment.objectUrl = URL.createObjectURL(blob);
}
} else {
attachment.objectUrl = URL.createObjectURL(blob);
}
attachment.available = true;
this.runtimeStore.touch();
void this.persistence.persistAttachmentMeta(attachment);
}
private isPlayableMedia(attachment: Pick<Attachment, 'mime'>): boolean {
return attachment.mime.startsWith('video/') || attachment.mime.startsWith('audio/');
}
private shouldReceiveToDisk(attachment: Attachment): boolean {
return this.isPlayableMedia(attachment) && !attachment.filePath && this.attachmentStorage.canWriteFiles();
}
private enqueueDiskFileChunk(
attachment: Attachment,
payload: ValidFileChunkPayload
): void {
const assemblyKey = `${payload.messageId}:${payload.fileId}`;
const previous = this.diskReceiveChains.get(assemblyKey) ?? Promise.resolve();
const next = previous
.catch(() => undefined)
.then(() => this.handleDiskFileChunk(attachment, assemblyKey, payload))
.catch((error: unknown) => this.handleDiskReceiveFailure(attachment, assemblyKey, error));
this.diskReceiveChains.set(assemblyKey, next);
void next.finally(() => {
if (this.diskReceiveChains.get(assemblyKey) === next) {
this.diskReceiveChains.delete(assemblyKey);
}
});
}
private async handleDiskFileChunk(
attachment: Attachment,
assemblyKey: string,
payload: ValidFileChunkPayload
): Promise<void> {
const decodedBytes = this.transport.decodeBase64(payload.data);
const requestKey = this.buildRequestKey(payload.messageId, payload.fileId);
this.runtimeStore.deletePendingRequest(requestKey);
this.clearAttachmentRequestError(attachment);
const assembly = await this.getOrCreateDiskReceiveAssembly(attachment, assemblyKey, payload.total);
if (!assembly) {
throw new Error('Could not prepare media download on disk.');
}
if (assembly.receivedIndexes.has(payload.index)) {
return;
}
if (payload.index !== assembly.receivedCount) {
throw new Error('Received media chunks out of order. Retry the download.');
}
const didAppend = await this.attachmentStorage.appendBase64(assembly.path, payload.data);
if (!didAppend) {
throw new Error('Could not write media download to disk.');
}
assembly.receivedIndexes.add(payload.index);
assembly.receivedCount += 1;
this.updateTransferProgress(attachment, decodedBytes, payload.fromPeerId);
this.runtimeStore.touch();
if (assembly.receivedCount < assembly.total && (attachment.receivedBytes ?? 0) < attachment.size) {
return;
}
const fileUrl = await this.attachmentStorage.getFileUrl(assembly.path);
if (!fileUrl) {
throw new Error('Could not open completed media download from disk.');
}
attachment.savedPath = assembly.path;
attachment.objectUrl = fileUrl;
attachment.available = true;
this.diskReceiveAssemblies.delete(assemblyKey);
this.runtimeStore.touch();
void this.persistence.persistAttachmentMeta(attachment);
}
private async getOrCreateDiskReceiveAssembly(
attachment: Attachment,
assemblyKey: string,
total: number
): Promise<DiskReceiveAssembly | null> {
const existing = this.diskReceiveAssemblies.get(assemblyKey);
if (existing) {
return existing;
}
const storageContainer = await this.persistence.resolveStorageContainerName(attachment);
const path = await this.attachmentStorage.createWritableFile(attachment, storageContainer);
if (!path) {
return null;
}
const assembly: DiskReceiveAssembly = {
path,
receivedCount: 0,
receivedIndexes: new Set<number>(),
total
};
this.diskReceiveAssemblies.set(assemblyKey, assembly);
return assembly;
}
private async handleDiskReceiveFailure(
attachment: Attachment,
assemblyKey: string,
error: unknown
): Promise<void> {
await this.deleteDiskReceiveAssembly(assemblyKey);
attachment.available = false;
attachment.objectUrl = undefined;
attachment.receivedBytes = 0;
attachment.speedBps = 0;
attachment.startedAtMs = undefined;
attachment.lastUpdateMs = undefined;
attachment.requestError = error instanceof Error && error.message
? error.message
: 'Media download failed. Retry the download.';
this.runtimeStore.touch();
}
private async deleteDiskReceiveAssembly(assemblyKey: string): Promise<void> {
const assembly = this.diskReceiveAssemblies.get(assemblyKey);
this.diskReceiveAssemblies.delete(assemblyKey);
if (assembly?.path) {
await this.attachmentStorage.deleteFile(assembly.path);
}
}
}