Disallow any types
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user