diff --git a/package.json b/package.json index 84cda2c..f775b28 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "lint-staged": "^15.0.0", "prettier": "^3.0.0", "typescript": "^5.0.0", - "typescript-eslint": "^8.18.2" + "typescript-eslint": "^8.18.2", + "@eslint/js": "^9.30.0" }, "husky": { "hooks": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2df5e77..384faf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: specifier: ^3.23.8 version: 3.25.67 devDependencies: + '@eslint/js': + specifier: ^9.30.0 + version: 9.30.0 '@types/debug': specifier: ^4.1.12 version: 4.1.12 diff --git a/src/hooks/useDownloader.ts b/src/hooks/useDownloader.ts index 811da03..754a6e1 100644 --- a/src/hooks/useDownloader.ts +++ b/src/hooks/useDownloader.ts @@ -207,8 +207,14 @@ export function useDownloader(uploaderPeerID: string): { console.error('[Downloader] no stream found for', message.fileName) return } - setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength) + const chunkSize = (message.bytes as ArrayBuffer).byteLength + setBytesDownloaded((bd) => bd + chunkSize) fileStream.enqueue(new Uint8Array(message.bytes as ArrayBuffer)) + dataConnection.send({ + type: MessageType.Ack, + fileName: message.fileName, + offset: message.offset + chunkSize, + } as z.infer) if (message.final) { console.log('[Downloader] finished receiving', message.fileName) fileStream.close() diff --git a/src/hooks/useUploaderConnections.ts b/src/hooks/useUploaderConnections.ts index f23d7ed..15be3ad 100644 --- a/src/hooks/useUploaderConnections.ts +++ b/src/hooks/useUploaderConnections.ts @@ -9,8 +9,28 @@ import { decodeMessage, Message, MessageType } from '../messages' import { getFileName } from '../fs' import { setRotating } from './useRotatingSpinner' -// TODO(@kern): Test for better values -const MAX_CHUNK_SIZE = 256 * 1024 // 256 KB +const MIN_CHUNK_SIZE = 128 * 1024 // 128 KB +const MAX_CHUNK_SIZE_CAP = 1024 * 1024 // 1 MB + +function calculateChunkSize(totalTransferSize: number): number { + const MIN_TRANSFER = 1 * 1024 * 1024 // 1 MB + const MAX_TRANSFER = 1 * 1024 * 1024 * 1024 // 1 GB + + if (totalTransferSize <= MIN_TRANSFER) { + return MIN_CHUNK_SIZE + } + + if (totalTransferSize >= MAX_TRANSFER) { + return MAX_CHUNK_SIZE_CAP + } + + const ratio = + (totalTransferSize - MIN_TRANSFER) / (MAX_TRANSFER - MIN_TRANSFER) + + return ( + MIN_CHUNK_SIZE + Math.round(ratio * (MAX_CHUNK_SIZE_CAP - MIN_CHUNK_SIZE)) + ) +} function validateOffset( files: UploadedFile[], @@ -31,6 +51,8 @@ export function useUploaderConnections( files: UploadedFile[], password: string, ): Array { + const totalTransferSize = files.reduce((sum, f) => sum + f.size, 0) + const MAX_CHUNK_SIZE = calculateChunkSize(totalTransferSize) const [connections, setConnections] = useState>([]) useEffect(() => { @@ -237,33 +259,10 @@ export function useUploaderConnections( final, } conn.send(request) - - updateConnection((draft) => { - offset = end - if (final) { - console.log( - '[UploaderConnections] completed file', - fileName, - '- file', - draft.completedFiles + 1, - 'of', - draft.totalFiles, - ) - return { - ...draft, - status: UploaderConnectionStatus.Ready, - completedFiles: draft.completedFiles + 1, - currentFileProgress: 0, - } - } else { - sendNextChunkAsync() - return { - ...draft, - uploadingOffset: end, - currentFileProgress: end / file.size, - } - } - }) + offset = end + if (!final) { + sendNextChunkAsync() + } }, 0) } @@ -309,6 +308,36 @@ export function useUploaderConnections( break } + case MessageType.Ack: { + const { fileName: ackFileName, offset: ackOffset } = message + try { + const ackFile = validateOffset(files, ackFileName, ackOffset) + updateConnection((draft) => { + if (draft.uploadingFileName !== ackFileName) { + return draft + } + + const completed = ackOffset >= ackFile.size + return { + ...draft, + uploadingOffset: ackOffset, + currentFileProgress: Math.min(ackOffset / ackFile.size, 1), + ...(completed + ? { + status: UploaderConnectionStatus.Ready, + completedFiles: draft.completedFiles + 1, + uploadingFileName: undefined, + } + : {}), + } + }) + } catch (err) { + console.error('[UploaderConnections] error handling ack:', err) + } + + break + } + case MessageType.Done: { console.log( '[UploaderConnections] transfer completed successfully', diff --git a/src/messages.ts b/src/messages.ts index a9acf2b..3072c6a 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -6,6 +6,7 @@ export enum MessageType { Start = 'Start', Chunk = 'Chunk', Pause = 'Pause', + Ack = 'Ack', Done = 'Done', Error = 'Error', PasswordRequired = 'PasswordRequired', @@ -48,6 +49,12 @@ export const ChunkMessage = z.object({ final: z.boolean(), }) +export const AckMessage = z.object({ + type: z.literal(MessageType.Ack), + fileName: z.string(), + offset: z.number(), +}) + export const DoneMessage = z.object({ type: z.literal(MessageType.Done), }) @@ -80,6 +87,7 @@ export const Message = z.discriminatedUnion('type', [ InfoMessage, StartMessage, ChunkMessage, + AckMessage, DoneMessage, ErrorMessage, PasswordRequiredMessage,