Skip to content

Commit 013f4ea

Browse files
authored
@uppy/transloadit: fix issue with allowMultipleUploadBatches (#5400)
* fix issue with allowMultipleUploadBatches fixes #5397 also refactor from promise.then to async/await and fix what seems like broken logic with recursive this.#afterUpload call * throw better error when all files have been canceled after an assembly has been created also rewrite #createAssembly to async/await * wait for updateAssembly when restoring fixes potential race condition
1 parent 98123d8 commit 013f4ea

File tree

2 files changed

+106
-99
lines changed

2 files changed

+106
-99
lines changed

e2e/cypress/integration/dashboard-transloadit.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ describe('Dashboard with Transloadit', () => {
6565
cy.get('.uppy-StatusBar-actionBtn--upload').click()
6666

6767
cy.wait(['@createAssemblies', '@tusCreate']).then(() => {
68-
const plugin = getPlugin(uppy)
68+
const { assembly } = getPlugin(uppy)
6969

70-
expect(plugin.assembly.closed).to.be.false
70+
expect(assembly.closed).to.be.false
7171

7272
uppy.cancelAll()
7373

7474
cy.wait(['@delete', '@tusDelete']).then(() => {
75-
expect(plugin.assembly.closed).to.be.true
75+
expect(assembly.closed).to.be.true
7676
})
7777
})
7878
})

packages/@uppy/transloadit/src/index.ts

+103-96
Original file line numberDiff line numberDiff line change
@@ -400,64 +400,64 @@ export default class Transloadit<
400400
return newFile
401401
}
402402

403-
#createAssembly(
403+
async #createAssembly(
404404
fileIDs: string[],
405405
assemblyOptions: OptionsWithRestructuredFields,
406406
) {
407407
this.uppy.log('[Transloadit] Create Assembly')
408408

409-
return this.client
410-
.createAssembly({
409+
try {
410+
const newAssembly = await this.client.createAssembly({
411411
...assemblyOptions,
412412
expectedFiles: fileIDs.length,
413413
})
414-
.then(async (newAssembly) => {
415-
const files = this.uppy
416-
.getFiles()
417-
.filter(({ id }) => fileIDs.includes(id))
418-
if (files.length === 0) {
419-
// All files have been removed, cancelling.
420-
await this.client.cancelAssembly(newAssembly)
421-
return null
422-
}
423-
424-
const assembly = new Assembly(newAssembly, this.#rateLimitedQueue)
425-
const { status } = assembly
426-
const assemblyID = status.assembly_id
427414

428-
const updatedFiles: Record<string, UppyFile<M, B>> = {}
429-
files.forEach((file) => {
430-
updatedFiles[file.id] = this.#attachAssemblyMetadata(file, status)
431-
})
415+
const files = this.uppy
416+
.getFiles()
417+
.filter(({ id }) => fileIDs.includes(id))
432418

433-
this.uppy.setState({
434-
files: {
435-
...this.uppy.getState().files,
436-
...updatedFiles,
437-
},
438-
})
419+
if (files.length === 0) {
420+
// All files have been removed, cancelling.
421+
await this.client.cancelAssembly(newAssembly)
422+
return null
423+
}
439424

440-
this.uppy.emit('transloadit:assembly-created', status, fileIDs)
425+
const assembly = new Assembly(newAssembly, this.#rateLimitedQueue)
426+
const { status } = assembly
427+
const assemblyID = status.assembly_id
441428

442-
this.uppy.log(`[Transloadit] Created Assembly ${assemblyID}`)
443-
return assembly
429+
const updatedFiles: Record<string, UppyFile<M, B>> = {}
430+
files.forEach((file) => {
431+
updatedFiles[file.id] = this.#attachAssemblyMetadata(file, status)
444432
})
445-
.catch((err) => {
446-
// TODO: use AssemblyError?
447-
const wrapped = new ErrorWithCause(
448-
`${this.i18n('creatingAssemblyFailed')}: ${err.message}`,
449-
{ cause: err },
450-
)
451-
if ('details' in err) {
452-
// @ts-expect-error details is not in the Error type
453-
wrapped.details = err.details
454-
}
455-
if ('assembly' in err) {
456-
// @ts-expect-error assembly is not in the Error type
457-
wrapped.assembly = err.assembly
458-
}
459-
throw wrapped
433+
434+
this.uppy.setState({
435+
files: {
436+
...this.uppy.getState().files,
437+
...updatedFiles,
438+
},
460439
})
440+
441+
this.uppy.emit('transloadit:assembly-created', status, fileIDs)
442+
443+
this.uppy.log(`[Transloadit] Created Assembly ${assemblyID}`)
444+
return assembly
445+
} catch (err) {
446+
// TODO: use AssemblyError?
447+
const wrapped = new ErrorWithCause(
448+
`${this.i18n('creatingAssemblyFailed')}: ${err.message}`,
449+
{ cause: err },
450+
)
451+
if ('details' in err) {
452+
// @ts-expect-error details is not in the Error type
453+
wrapped.details = err.details
454+
}
455+
if ('assembly' in err) {
456+
// @ts-expect-error assembly is not in the Error type
457+
wrapped.assembly = err.assembly
458+
}
459+
throw wrapped
460+
}
461461
}
462462

463463
#createAssemblyWatcher(idOrArrayOfIds: string | string[]) {
@@ -616,6 +616,7 @@ export default class Transloadit<
616616
await this.client.cancelAssembly(assembly)
617617
// TODO bubble this through AssemblyWatcher so its event handlers can clean up correctly
618618
this.uppy.emit('transloadit:assembly-cancelled', assembly)
619+
this.assembly = undefined
619620
}
620621

621622
/**
@@ -702,20 +703,21 @@ export default class Transloadit<
702703
this.#connectAssembly(this.assembly!)
703704
}
704705

705-
// Force-update all Assemblies to check for missed events.
706-
const updateAssemblies = () => {
706+
// Force-update Assembly to check for missed events.
707+
const updateAssembly = () => {
707708
return this.assembly?.update()
708709
}
709710

710711
// Restore all Assembly state.
711-
this.restored = Promise.resolve().then(() => {
712+
this.restored = (async () => {
712713
restoreState()
713714
restoreAssemblies()
714-
updateAssemblies()
715-
})
716-
717-
this.restored.then(() => {
715+
await updateAssembly()
718716
this.restored = null
717+
})()
718+
719+
this.restored.catch((err) => {
720+
this.uppy.log('Failed to restore', err)
719721
})
720722
}
721723

@@ -800,8 +802,11 @@ export default class Transloadit<
800802
try {
801803
const assembly =
802804
// this.assembly can already be defined if we recovered files with Golden Retriever (this.#onRestored)
803-
(this.assembly ??
804-
(await this.#createAssembly(fileIDs, assemblyOptions)))!
805+
this.assembly ?? (await this.#createAssembly(fileIDs, assemblyOptions))
806+
807+
if (assembly == null)
808+
throw new Error('All files were canceled after assembly was created')
809+
805810
if (this.opts.importFromUploadURLs) {
806811
await this.#reserveFiles(assembly, fileIDs)
807812
}
@@ -823,65 +828,67 @@ export default class Transloadit<
823828
}
824829
}
825830

826-
#afterUpload = (fileIDs: string[], uploadID: string): Promise<void> => {
827-
const files = fileIDs.map((fileID) => this.uppy.getFile(fileID))
828-
// Only use files without errors
829-
const filteredFileIDs = files
830-
.filter((file) => !file.error)
831-
.map((file) => file.id)
831+
#afterUpload = async (fileIDs: string[], uploadID: string): Promise<void> => {
832+
try {
833+
// If we're still restoring state, wait for that to be done.
834+
await this.restored
832835

833-
// If we're still restoring state, wait for that to be done.
834-
if (this.restored) {
835-
return this.restored.then(() => {
836-
return this.#afterUpload(filteredFileIDs, uploadID)
837-
})
838-
}
836+
const files = fileIDs
837+
.map((fileID) => this.uppy.getFile(fileID))
838+
// Only use files without errors
839+
.filter((file) => !file.error)
839840

840-
const assemblyID = this.assembly?.status.assembly_id
841+
const assemblyID = this.assembly?.status.assembly_id
841842

842-
const closeSocketConnections = () => {
843-
this.assembly?.close()
844-
}
843+
const closeSocketConnections = () => {
844+
this.assembly?.close()
845+
}
845846

846-
// If we don't have to wait for encoding metadata or results, we can close
847-
// the socket immediately and finish the upload.
848-
if (!this.#shouldWaitAfterUpload()) {
849-
closeSocketConnections()
850-
const status = this.assembly?.status
851-
if (status != null) {
852-
this.uppy.addResultData(uploadID, {
853-
transloadit: [status],
854-
})
847+
// If we don't have to wait for encoding metadata or results, we can close
848+
// the socket immediately and finish the upload.
849+
if (!this.#shouldWaitAfterUpload()) {
850+
closeSocketConnections()
851+
const status = this.assembly?.status
852+
if (status != null) {
853+
this.uppy.addResultData(uploadID, {
854+
transloadit: [status],
855+
})
856+
}
857+
return
855858
}
856-
return Promise.resolve()
857-
}
858859

859-
// If no Assemblies were created for this upload, we also do not have to wait.
860-
// There's also no sockets or anything to close, so just return immediately.
861-
if (!assemblyID) {
862-
this.uppy.addResultData(uploadID, { transloadit: [] })
863-
return Promise.resolve()
864-
}
860+
// If no Assemblies were created for this upload, we also do not have to wait.
861+
// There's also no sockets or anything to close, so just return immediately.
862+
if (!assemblyID) {
863+
this.uppy.addResultData(uploadID, { transloadit: [] })
864+
return
865+
}
865866

866-
const incompleteFiles = files.filter(
867-
(file) => !hasProperty(this.completedFiles, file.id),
868-
)
869-
incompleteFiles.forEach((file) => {
870-
this.uppy.emit('postprocess-progress', file, {
871-
mode: 'indeterminate',
872-
message: this.i18n('encoding'),
867+
const incompleteFiles = files.filter(
868+
(file) => !hasProperty(this.completedFiles, file.id),
869+
)
870+
incompleteFiles.forEach((file) => {
871+
this.uppy.emit('postprocess-progress', file, {
872+
mode: 'indeterminate',
873+
message: this.i18n('encoding'),
874+
})
873875
})
874-
})
875876

876-
return this.#watcher.promise.then(() => {
877+
await this.#watcher.promise
878+
// assembly is now done processing!
877879
closeSocketConnections()
878880
const status = this.assembly?.status
879881
if (status != null) {
880882
this.uppy.addResultData(uploadID, {
881883
transloadit: [status],
882884
})
883885
}
884-
})
886+
} finally {
887+
// in case allowMultipleUploadBatches is true and the user wants to upload again,
888+
// we need to allow a new assembly to be created.
889+
// see https://github.com/transloadit/uppy/issues/5397
890+
this.assembly = undefined
891+
}
885892
}
886893

887894
#closeAssemblyIfExists = () => {

0 commit comments

Comments
 (0)