From 248df834c1526d7556f68662a5314b256b941b64 Mon Sep 17 00:00:00 2001 From: Alex Kern Date: Sat, 28 Dec 2024 12:32:01 -0800 Subject: [PATCH] more progress --- next.config.js | 4 + src/components/ConnectionListItem.tsx | 49 +++++++---- src/components/ErrorMessage.tsx | 2 +- src/hooks/useDownloader.ts | 9 +- src/hooks/useUploaderChannel.ts | 20 +++++ src/hooks/useUploaderConnections.ts | 113 ++++++++++++++------------ src/types.ts | 1 + 7 files changed, 127 insertions(+), 71 deletions(-) diff --git a/next.config.js b/next.config.js index a5788fb..82fe48b 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,9 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + // Disable strict mode to avoid calling useEffect twice in development. + // The uploader and downloader are both using useEffect to listen for peerjs events + // which causes the connection to be created twice. + reactStrictMode: false, } module.exports = nextConfig \ No newline at end of file diff --git a/src/components/ConnectionListItem.tsx b/src/components/ConnectionListItem.tsx index adeb038..26acc5d 100644 --- a/src/components/ConnectionListItem.tsx +++ b/src/components/ConnectionListItem.tsx @@ -17,6 +17,8 @@ export function ConnectionListItem({ return 'bg-green-500 dark:bg-green-600' case UploaderConnectionStatus.Closed: return 'bg-red-500 dark:bg-red-600' + case UploaderConnectionStatus.InvalidPassword: + return 'bg-red-500 dark:bg-red-600' default: return 'bg-stone-500 dark:bg-stone-600' } @@ -24,24 +26,37 @@ export function ConnectionListItem({ return (
-
- - {conn.browserName && conn.browserVersion ? ( - <> - {conn.browserName}{' '} - v{conn.browserVersion} - - ) : ( - 'Downloader' +
+
+ + {conn.browserName && conn.browserVersion ? ( + <> + {conn.browserName}{' '} + v{conn.browserVersion} + + ) : ( + 'Downloader' + )} + + + {conn.status.replace(/_/g, ' ')} + +
+ +
+
+ Completed: {conn.completedFiles} / {conn.totalFiles} files +
+ {conn.uploadingFileName && conn.status === UploaderConnectionStatus.Uploading && ( +
+ Current file: {Math.round(conn.currentFileProgress * 100)}% +
)} - - - {conn.status} - +
{message} diff --git a/src/hooks/useDownloader.ts b/src/hooks/useDownloader.ts index fcf7d8b..ca0f08e 100644 --- a/src/hooks/useDownloader.ts +++ b/src/hooks/useDownloader.ts @@ -122,7 +122,14 @@ export function useDownloader(uploaderPeerID: string): { peer.on('error', handleError) return () => { - if (conn.open) conn.close() + if (conn.open) { + conn.close() + } else { + conn.once('open', () => { + conn.close() + }) + } + conn.off('open', handleOpen) conn.off('data', handleData) conn.off('error', handleError) diff --git a/src/hooks/useUploaderChannel.ts b/src/hooks/useUploaderChannel.ts index d3dcf77..d12a03a 100644 --- a/src/hooks/useUploaderChannel.ts +++ b/src/hooks/useUploaderChannel.ts @@ -81,6 +81,26 @@ export function useUploaderChannel( } }, [secret, shortSlug, renewMutation, renewInterval]) + useEffect(() => { + if (!shortSlug || !secret) return + + const handleUnload = (): void => { + // Using sendBeacon for best-effort delivery during page unload + navigator.sendBeacon( + '/api/destroy', + JSON.stringify({ slug: shortSlug }) + ) + } + + window.addEventListener('beforeunload', handleUnload) + window.addEventListener('unload', handleUnload) + + return () => { + window.removeEventListener('beforeunload', handleUnload) + window.removeEventListener('unload', handleUnload) + } + }, [shortSlug, secret]) + return { isLoading, error, diff --git a/src/hooks/useUploaderConnections.ts b/src/hooks/useUploaderConnections.ts index e110f65..4536a2e 100644 --- a/src/hooks/useUploaderConnections.ts +++ b/src/hooks/useUploaderConnections.ts @@ -32,9 +32,9 @@ export function useUploaderConnections( ): Array { const [connections, setConnections] = useState>([]) - console.log('connections', connections) - useEffect(() => { + const cleanupHandlers: Array<() => void> = [] + const listener = (conn: DataConnection) => { // If the connection is a report, we need to hard-redirect the uploader to the reported page to prevent them from uploading more files. if (conn.metadata?.type === 'report') { @@ -61,14 +61,9 @@ export function useUploaderConnections( } setConnections((conns) => { - // Check if connection already exists - const exists = conns.some(conn => conn.dataConnection === newConn.dataConnection) - if (exists) { - console.log('connection already exists!', newConn.dataConnection) - return conns - } return [newConn, ...conns] }) + const updateConnection = ( fn: (c: UploaderConnection) => UploaderConnection, ) => { @@ -77,7 +72,7 @@ export function useUploaderConnections( ) } - conn.on('data', (data): void => { + const onData = (data: any): void => { try { const message = decodeMessage(data) switch (message.type) { @@ -120,7 +115,7 @@ export function useUploaderConnections( return { ...draft, ...newConnectionState, - status: UploaderConnectionStatus.Paused, + status: UploaderConnectionStatus.Ready, } }) @@ -146,14 +141,15 @@ export function useUploaderConnections( if (submittedPassword === password) { updateConnection((draft) => { if ( - draft.status !== UploaderConnectionStatus.Authenticating + draft.status !== UploaderConnectionStatus.Authenticating && + draft.status !== UploaderConnectionStatus.InvalidPassword ) { return draft } return { ...draft, - status: UploaderConnectionStatus.Paused, + status: UploaderConnectionStatus.Ready, } }) @@ -196,11 +192,50 @@ export function useUploaderConnections( const fileName = message.fileName let offset = message.offset const file = validateOffset(files, fileName, offset) + + const sendNextChunkAsync = () => { + sendChunkTimeout = setTimeout(() => { + const end = Math.min(file.size, offset + MAX_CHUNK_SIZE) + const chunkSize = end - offset + const final = chunkSize < MAX_CHUNK_SIZE + const request: Message = { + type: MessageType.Chunk, + fileName, + offset, + bytes: file.slice(offset, end), + final, + } + conn.send(request) + + updateConnection((draft) => { + offset = end + if (final) { + console.log('final chunk', draft.completedFiles + 1) + return { + ...draft, + status: UploaderConnectionStatus.Ready, + completedFiles: draft.completedFiles + 1, + currentFileProgress: 0, + } + } else { + sendNextChunkAsync() + return { + ...draft, + uploadingOffset: end, + currentFileProgress: end / file.size, + } + } + }) + }, 0) + } + updateConnection((draft) => { - if (draft.status !== UploaderConnectionStatus.Paused) { + if (draft.status !== UploaderConnectionStatus.Ready && draft.status !== UploaderConnectionStatus.Paused) { return draft } + sendNextChunkAsync() + return { ...draft, status: UploaderConnectionStatus.Uploading, @@ -210,42 +245,6 @@ export function useUploaderConnections( } }) - const sendNextChunk = () => { - const end = Math.min(file.size, offset + MAX_CHUNK_SIZE) - const chunkSize = end - offset - const final = chunkSize < MAX_CHUNK_SIZE - const request: Message = { - type: MessageType.Chunk, - fileName, - offset, - bytes: file.slice(offset, end), - final, - } - conn.send(request) - - updateConnection((draft) => { - offset = end - if (final) { - return { - ...draft, - status: UploaderConnectionStatus.Paused, - completedFiles: draft.completedFiles + 1, - currentFileProgress: 0, - } - } else { - sendChunkTimeout = setTimeout(() => { - sendNextChunk() - }, 0) - return { - ...draft, - uploadingOffset: end, - currentFileProgress: end / file.size, - } - } - }) - } - sendNextChunk() - break } @@ -270,7 +269,7 @@ export function useUploaderConnections( case MessageType.Done: { updateConnection((draft) => { - if (draft.status !== UploaderConnectionStatus.Paused) { + if (draft.status !== UploaderConnectionStatus.Ready) { return draft } @@ -286,9 +285,9 @@ export function useUploaderConnections( } catch (err) { console.error(err) } - }) + } - conn.on('close', (): void => { + const onClose = (): void => { if (sendChunkTimeout) { clearTimeout(sendChunkTimeout) } @@ -308,6 +307,15 @@ export function useUploaderConnections( status: UploaderConnectionStatus.Closed, } }) + } + + conn.on('data', onData) + conn.on('close', onClose) + + cleanupHandlers.push(() => { + conn.off('data', onData) + conn.off('close', onClose) + conn.close() }) } @@ -315,6 +323,7 @@ export function useUploaderConnections( return () => { peer.off('connection', listener) + cleanupHandlers.forEach((fn) => fn()) } }, [peer, files, password]) diff --git a/src/types.ts b/src/types.ts index 973331e..3261432 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ export type UploadedFile = File & { entryFullPath?: string } export enum UploaderConnectionStatus { Pending = 'PENDING', + Ready = 'READY', Paused = 'PAUSED', Uploading = 'UPLOADING', Done = 'DONE',