Disallow any types

This commit is contained in:
2026-03-09 23:02:52 +01:00
parent 3b1aab4985
commit dc6746c882
40 changed files with 961 additions and 476 deletions

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion, complexity, max-statements-per-line */
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-non-null-assertion, complexity, max-statements-per-line */
import {
Injectable,
inject,
@@ -12,6 +12,11 @@ import { Store } from '@ngrx/store';
import { selectCurrentRoomName } from '../../store/rooms/rooms.selectors';
import { DatabaseService } from './database.service';
import { recordDebugNetworkFileChunk } from './debug-network-metrics.service';
import type {
ChatAttachmentAnnouncement,
ChatAttachmentMeta,
ChatEvent
} from '../models/index';
/** Size (bytes) of each chunk when streaming a file over RTCDataChannel. */
const FILE_CHUNK_SIZE_BYTES = 64 * 1024; // 64 KB
@@ -37,26 +42,7 @@ const FILE_NOT_FOUND_REQUEST_ERROR = 'The connected peers do not have this file
/**
* Metadata describing a file attachment linked to a chat message.
*/
export interface AttachmentMeta {
/** Unique attachment identifier. */
id: string;
/** ID of the parent message. */
messageId: string;
/** Original file name. */
filename: string;
/** File size in bytes. */
size: number;
/** MIME type (e.g. `image/png`). */
mime: string;
/** Whether the file is a raster/vector image. */
isImage: boolean;
/** Peer ID of the user who originally uploaded the file. */
uploaderPeerId?: string;
/** Electron-only: absolute path to the uploader's original file. */
filePath?: string;
/** Electron-only: disk-cache path where the file was saved locally. */
savedPath?: string;
}
export type AttachmentMeta = ChatAttachmentMeta;
/**
* Runtime representation of an attachment including download
@@ -79,6 +65,72 @@ export interface Attachment extends AttachmentMeta {
requestError?: string;
}
type FileAnnounceEvent = ChatEvent & {
type: 'file-announce';
messageId: string;
file: ChatAttachmentAnnouncement;
};
type FileChunkEvent = ChatEvent & {
type: 'file-chunk';
messageId: string;
fileId: string;
index: number;
total: number;
data: string;
fromPeerId?: string;
};
type FileRequestEvent = ChatEvent & {
type: 'file-request';
messageId: string;
fileId: string;
fromPeerId?: string;
};
type FileCancelEvent = ChatEvent & {
type: 'file-cancel';
messageId: string;
fileId: string;
fromPeerId?: string;
};
type FileNotFoundEvent = ChatEvent & {
type: 'file-not-found';
messageId: string;
fileId: string;
};
type FileAnnouncePayload = Pick<ChatEvent, 'messageId' | 'file'>;
interface FileChunkPayload {
messageId?: string;
fileId?: string;
fromPeerId?: string;
index?: number;
total?: number;
data?: ChatEvent['data'];
}
type FileRequestPayload = Pick<ChatEvent, 'messageId' | 'fileId' | 'fromPeerId'>;
type FileCancelPayload = Pick<ChatEvent, 'messageId' | 'fileId' | 'fromPeerId'>;
type FileNotFoundPayload = Pick<ChatEvent, 'messageId' | 'fileId'>;
interface AttachmentElectronApi {
getAppDataPath?: () => Promise<string>;
fileExists?: (filePath: string) => Promise<boolean>;
readFile?: (filePath: string) => Promise<string>;
deleteFile?: (filePath: string) => Promise<boolean>;
ensureDir?: (dirPath: string) => Promise<boolean>;
writeFile?: (filePath: string, data: string) => Promise<boolean>;
}
type ElectronWindow = Window & {
electronAPI?: AttachmentElectronApi;
};
type LocalFileWithPath = File & {
path?: string;
};
/**
* Manages peer-to-peer file transfer, local persistence, and
* in-memory caching of file attachments linked to chat messages.
@@ -140,6 +192,10 @@ export class AttachmentService {
});
}
private getElectronApi(): AttachmentElectronApi | undefined {
return (window as ElectronWindow).electronAPI;
}
/** Return the attachment list for a given message. */
getForMessage(messageId: string): Attachment[] {
return this.attachmentsByMessage.get(messageId) ?? [];
@@ -285,7 +341,7 @@ export class AttachmentService {
/**
* Handle a `file-not-found` response - try the next available peer.
*/
handleFileNotFound(payload: any): void {
handleFileNotFound(payload: FileNotFoundPayload): void {
const { messageId, fileId } = payload;
if (!messageId || !fileId)
@@ -345,7 +401,7 @@ export class AttachmentService {
mime: file.type || DEFAULT_MIME_TYPE,
isImage: file.type.startsWith('image/'),
uploaderPeerId,
filePath: (file as any)?.path,
filePath: (file as LocalFileWithPath).path,
available: false
};
@@ -366,7 +422,7 @@ export class AttachmentService {
}
// Broadcast metadata to peers
this.webrtc.broadcastMessage({
const fileAnnounceEvent: FileAnnounceEvent = {
type: 'file-announce',
messageId,
file: {
@@ -377,7 +433,9 @@ export class AttachmentService {
isImage: attachment.isImage,
uploaderPeerId
}
} as any);
};
this.webrtc.broadcastMessage(fileAnnounceEvent);
// Auto-stream small inline-preview media
if (this.isMedia(attachment) && attachment.size <= MAX_AUTO_SAVE_SIZE_BYTES) {
@@ -396,7 +454,7 @@ export class AttachmentService {
}
/** Handle a `file-announce` event from a peer. */
handleFileAnnounce(payload: any): void {
handleFileAnnounce(payload: FileAnnouncePayload): void {
const { messageId, file } = payload;
if (!messageId || !file)
@@ -433,14 +491,14 @@ export class AttachmentService {
* expected count is reached, at which point the buffers are
* assembled into a Blob and an object URL is created.
*/
handleFileChunk(payload: any): void {
handleFileChunk(payload: FileChunkPayload): void {
const { messageId, fileId, fromPeerId, index, total, data } = payload;
if (
!messageId || !fileId ||
typeof index !== 'number' ||
typeof total !== 'number' ||
!data
typeof data !== 'string'
)
return;
@@ -538,7 +596,7 @@ export class AttachmentService {
* If none of these sources has the file, a `file-not-found`
* message is sent so the requester can try another peer.
*/
async handleFileRequest(payload: any): Promise<void> {
async handleFileRequest(payload: FileRequestPayload): Promise<void> {
const { messageId, fileId, fromPeerId } = payload;
if (!messageId || !fileId || !fromPeerId)
@@ -566,7 +624,7 @@ export class AttachmentService {
const list = this.attachmentsByMessage.get(messageId) ?? [];
const attachment = list.find((entry) => entry.id === fileId);
const electronApi = (window as any)?.electronAPI;
const electronApi = this.getElectronApi();
// 2. Electron filePath
if (attachment?.filePath && electronApi?.fileExists && electronApi?.readFile) {
@@ -619,11 +677,13 @@ export class AttachmentService {
}
// 5. File not available locally
this.webrtc.sendToPeer(fromPeerId, {
const fileNotFoundEvent: FileNotFoundEvent = {
type: 'file-not-found',
messageId,
fileId
} as any);
};
this.webrtc.sendToPeer(fromPeerId, fileNotFoundEvent);
}
/**
@@ -658,11 +718,13 @@ export class AttachmentService {
this.touch();
// Notify uploader to stop streaming
this.webrtc.sendToPeer(targetPeerId, {
const fileCancelEvent: FileCancelEvent = {
type: 'file-cancel',
messageId,
fileId: attachment.id
} as any);
};
this.webrtc.sendToPeer(targetPeerId, fileCancelEvent);
} catch { /* best-effort */ }
}
@@ -670,7 +732,7 @@ export class AttachmentService {
* Handle a `file-cancel` from the requester - record the
* cancellation so the streaming loop breaks early.
*/
handleFileCancel(payload: any): void {
handleFileCancel(payload: FileCancelPayload): void {
const { messageId, fileId, fromPeerId } = payload;
if (!messageId || !fileId || !fromPeerId)
@@ -774,7 +836,7 @@ export class AttachmentService {
}
private async deleteSavedFile(filePath: string): Promise<void> {
const electronApi = (window as any)?.electronAPI;
const electronApi = this.getElectronApi();
if (!electronApi?.deleteFile)
return;
@@ -842,11 +904,13 @@ export class AttachmentService {
triedPeers.add(targetPeerId);
this.pendingRequests.set(requestKey, triedPeers);
this.webrtc.sendToPeer(targetPeerId, {
const fileRequestEvent: FileRequestEvent = {
type: 'file-request',
messageId,
fileId
} as any);
};
this.webrtc.sendToPeer(targetPeerId, fileRequestEvent);
return true;
}
@@ -866,15 +930,16 @@ export class AttachmentService {
const slice = file.slice(offset, offset + FILE_CHUNK_SIZE_BYTES);
const arrayBuffer = await slice.arrayBuffer();
const base64 = this.arrayBufferToBase64(arrayBuffer);
this.webrtc.broadcastMessage({
const fileChunkEvent: FileChunkEvent = {
type: 'file-chunk',
messageId,
fileId,
index: chunkIndex,
total: totalChunks,
data: base64
} as any);
};
this.webrtc.broadcastMessage(fileChunkEvent);
offset += FILE_CHUNK_SIZE_BYTES;
chunkIndex++;
@@ -900,15 +965,16 @@ export class AttachmentService {
const slice = file.slice(offset, offset + FILE_CHUNK_SIZE_BYTES);
const arrayBuffer = await slice.arrayBuffer();
const base64 = this.arrayBufferToBase64(arrayBuffer);
await this.webrtc.sendToPeerBuffered(targetPeerId, {
const fileChunkEvent: FileChunkEvent = {
type: 'file-chunk',
messageId,
fileId,
index: chunkIndex,
total: totalChunks,
data: base64
} as any);
};
await this.webrtc.sendToPeerBuffered(targetPeerId, fileChunkEvent);
offset += FILE_CHUNK_SIZE_BYTES;
chunkIndex++;
@@ -925,7 +991,11 @@ export class AttachmentService {
fileId: string,
diskPath: string
): Promise<void> {
const electronApi = (window as any)?.electronAPI;
const electronApi = this.getElectronApi();
if (!electronApi?.readFile)
return;
const base64Full = await electronApi.readFile(diskPath);
const fileBytes = this.base64ToUint8Array(base64Full);
const totalChunks = Math.ceil(fileBytes.byteLength / FILE_CHUNK_SIZE_BYTES);
@@ -942,15 +1012,16 @@ export class AttachmentService {
slice.byteOffset + slice.byteLength
);
const base64Chunk = this.arrayBufferToBase64(sliceBuffer);
this.webrtc.sendToPeer(targetPeerId, {
const fileChunkEvent: FileChunkEvent = {
type: 'file-chunk',
messageId,
fileId,
index: chunkIndex,
total: totalChunks,
data: base64Chunk
} as any);
};
this.webrtc.sendToPeer(targetPeerId, fileChunkEvent);
}
}
@@ -960,10 +1031,10 @@ export class AttachmentService {
*/
private async saveFileToDisk(attachment: Attachment, blob: Blob): Promise<void> {
try {
const electronApi = (window as any)?.electronAPI;
const electronApi = this.getElectronApi();
const appDataPath: string | undefined = await electronApi?.getAppDataPath?.();
if (!appDataPath)
if (!appDataPath || !electronApi?.ensureDir || !electronApi.writeFile)
return;
const roomName = await this.resolveCurrentRoomName();
@@ -992,7 +1063,7 @@ export class AttachmentService {
/** On startup, try loading previously saved files from disk (Electron). */
private async tryLoadSavedFiles(): Promise<void> {
const electronApi = (window as any)?.electronAPI;
const electronApi = this.getElectronApi();
if (!electronApi?.fileExists || !electronApi?.readFile)
return;