Skip to content

Commit 8d9c7f9

Browse files
authored
feat: Added clear mechanism to remove corrupted uploads (#439)
1 parent 6ddc7b4 commit 8d9c7f9

File tree

3 files changed

+52
-9
lines changed

3 files changed

+52
-9
lines changed

src/components/UploadProgress.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export const UploadProgress = ({
4242
onCancel?: React.MouseEventHandler<HTMLButtonElement>
4343
text?: React.ReactNode
4444
}) => {
45+
// Disable cancel button when upload is 90% or more complete
46+
// to prevent inconsistency between Mux and Sanity
47+
const isCancelDisabled = progress >= 90
48+
4549
return (
4650
<CardWrapper tone="primary" padding={4} border height="fill">
4751
<FlexWrapper align="center" justify="space-between" height="fill" direction="row" gap={2}>
@@ -67,6 +71,7 @@ export const UploadProgress = ({
6771
mode="ghost"
6872
tone="critical"
6973
onClick={onCancel}
74+
disabled={isCancelDisabled}
7075
/>
7176
) : null}
7277
</FlexWrapper>

src/components/Uploader.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default function Uploader(props: Props) {
9292
).current
9393

9494
const uploadRef = useRef<Subscription | null>(null)
95+
const uploadingDocumentId = useRef<string | null>(null)
9596
const [state, dispatch] = useReducer(
9697
(prev: State, action: UploaderStateAction) => {
9798
switch (action.action) {
@@ -121,11 +122,13 @@ export default function Uploader(props: Props) {
121122
// Clear upload observable on completion
122123
uploadRef.current?.unsubscribe()
123124
uploadRef.current = null
125+
uploadingDocumentId.current = null
124126
return INITIAL_STATE
125127
case 'error':
126128
// Clear upload observable on error
127129
uploadRef.current?.unsubscribe()
128130
uploadRef.current = null
131+
uploadingDocumentId.current = null
129132
return Object.assign({}, INITIAL_STATE, {error: action.error})
130133
default:
131134
return prev
@@ -139,13 +142,38 @@ export default function Uploader(props: Props) {
139142
)
140143

141144
// Make sure we close out the upload observer on dismount
145+
// and cleanup orphaned documents if upload was in progress
142146
useEffect(() => {
143-
return () => {
147+
const cleanup = () => {
148+
// Cancel subscription
144149
if (uploadRef.current && !uploadRef.current.closed) {
145150
uploadRef.current.unsubscribe()
146151
}
152+
153+
// Delete orphaned document if upload was in progress and document is different from the saved asset
154+
if (uploadingDocumentId.current && props.asset?._id !== uploadingDocumentId.current) {
155+
const docId = uploadingDocumentId.current
156+
uploadingDocumentId.current = null
157+
158+
props.client.delete(docId).catch((err) => {
159+
console.warn('Failed to cleanup orphaned upload document:', err)
160+
})
161+
}
162+
}
163+
164+
const handleBeforeUnload = () => {
165+
cleanup()
147166
}
148-
}, [])
167+
168+
window.addEventListener('beforeunload', handleBeforeUnload)
169+
window.addEventListener('pagehide', handleBeforeUnload)
170+
171+
return () => {
172+
window.removeEventListener('beforeunload', handleBeforeUnload)
173+
window.removeEventListener('pagehide', handleBeforeUnload)
174+
cleanup()
175+
}
176+
}, [props.client, props.asset?._id])
149177

150178
/* -------------------------------------------------------------------------- */
151179
/* Uploading */
@@ -183,8 +211,9 @@ export default function Uploader(props: Props) {
183211
takeUntil(
184212
cancelUploadButton.observable.pipe(
185213
tap(() => {
186-
if (state.uploadStatus?.uuid) {
187-
props.client.delete(state.uploadStatus.uuid)
214+
if (uploadingDocumentId.current) {
215+
props.client.delete(uploadingDocumentId.current)
216+
uploadingDocumentId.current = null
188217
}
189218
})
190219
)
@@ -196,6 +225,10 @@ export default function Uploader(props: Props) {
196225
next: (event) => {
197226
switch (event.type) {
198227
case 'uuid':
228+
// Track the document ID for cleanup on unmount
229+
uploadingDocumentId.current = event.uuid
230+
dispatch({action: 'progressInfo', ...event})
231+
break
199232
case 'file':
200233
case 'url':
201234
dispatch({action: 'progressInfo', ...event})
@@ -205,6 +238,7 @@ export default function Uploader(props: Props) {
205238
break
206239
case 'success':
207240
dispatch({action: 'progress', percent: 100})
241+
uploadingDocumentId.current = null
208242
props.onChange(
209243
PatchEvent.from([
210244
setIfMissing({asset: {}}),

src/components/VideoPlayer.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,23 @@ export default function VideoPlayer({
2626

2727
const isAudio = assetIsAudio(asset)
2828
const muxPlayer = useRef<MuxPlayerRefAttributes>(null)
29-
const thumbnail = getPosterSrc({asset, client, width: thumbnailWidth})
3029

31-
const {src: videoSrc, error} = useMemo(() => {
30+
const {
31+
src: videoSrc,
32+
thumbnail: thumbnailSrc,
33+
error,
34+
} = useMemo(() => {
3235
try {
36+
const thumbnail = getPosterSrc({asset, client, width: thumbnailWidth})
3337
const src = asset?.playbackId && getVideoSrc({client, asset})
34-
if (src) return {src: src}
38+
if (src) return {src: src, thumbnail}
3539

3640
return {error: new TypeError('Asset has no playback ID')}
3741
// eslint-disable-next-line @typescript-eslint/no-shadow
3842
} catch (error) {
3943
return {error}
4044
}
41-
}, [asset, client])
45+
}, [asset, client, thumbnailWidth])
4246

4347
const signedToken = useMemo(() => {
4448
try {
@@ -66,7 +70,7 @@ export default function VideoPlayer({
6670
{videoSrc && (
6771
<>
6872
<MuxPlayer
69-
poster={thumbnail}
73+
poster={thumbnailSrc}
7074
ref={muxPlayer}
7175
{...props}
7276
playsInline

0 commit comments

Comments
 (0)