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',