Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 62a8c69

Browse files
authored
Merge pull request #88 from ckeditor/t/87
Fix: `FileLoader` now accepts `Promise` instead of a `File` instance. Closes #87. BREAKING CHANGE: The `FileLoader.file` property was changed to a getter which returns a native `Promise` instance instead of a `File` instance. The returned promise resolves to a `File` instance.
2 parents 6bd50dd + 041f22b commit 62a8c69

File tree

3 files changed

+406
-63
lines changed

3 files changed

+406
-63
lines changed

src/filereader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default class FileReader {
5757
* Reads the provided file.
5858
*
5959
* @param {File} file Native File object.
60-
* @returns {Promise} Returns a promise that will be resolved with file's content.
60+
* @returns {Promise.<String>} Returns a promise that will be resolved with file's content.
6161
* The promise will be rejected in case of an error or when the reading process is aborted.
6262
*/
6363
read( file ) {

src/filerepository.js

Lines changed: 117 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export default class FileRepository extends Plugin {
6464
this.loaders.on( 'add', () => this._updatePendingAction() );
6565
this.loaders.on( 'remove', () => this._updatePendingAction() );
6666

67+
/**
68+
* Loaders mappings used to retrieve loaders references.
69+
*
70+
* @private
71+
* @member {Map<File|Promise, FileLoader>} #_loadersMap
72+
*/
73+
this._loadersMap = new Map();
74+
6775
/**
6876
* Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
6977
* while upload is in progress. When there is no upload then value is `null`.
@@ -120,32 +128,26 @@ export default class FileRepository extends Plugin {
120128
}
121129

122130
/**
123-
* Returns the loader associated with specified file.
131+
* Returns the loader associated with specified file or promise.
124132
*
125133
* To get loader by id use `fileRepository.loaders.get( id )`.
126134
*
127-
* @param {File} file Native file handle.
135+
* @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
128136
* @returns {module:upload/filerepository~FileLoader|null}
129137
*/
130-
getLoader( file ) {
131-
for ( const loader of this.loaders ) {
132-
if ( loader.file == file ) {
133-
return loader;
134-
}
135-
}
136-
137-
return null;
138+
getLoader( fileOrPromise ) {
139+
return this._loadersMap.get( fileOrPromise ) || null;
138140
}
139141

140142
/**
141143
* Creates a loader instance for the given file.
142144
*
143145
* Requires {@link #createUploadAdapter} factory to be defined.
144146
*
145-
* @param {File} file Native File object.
147+
* @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
146148
* @returns {module:upload/filerepository~FileLoader|null}
147149
*/
148-
createLoader( file ) {
150+
createLoader( fileOrPromise ) {
149151
if ( !this.createUploadAdapter ) {
150152
/**
151153
* You need to enable an upload adapter in order to be able to upload files.
@@ -174,10 +176,17 @@ export default class FileRepository extends Plugin {
174176
return null;
175177
}
176178

177-
const loader = new FileLoader( file );
178-
loader._adapter = this.createUploadAdapter( loader );
179+
const loader = new FileLoader( Promise.resolve( fileOrPromise ), this.createUploadAdapter );
179180

180181
this.loaders.add( loader );
182+
this._loadersMap.set( fileOrPromise, loader );
183+
184+
// Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
185+
if ( fileOrPromise instanceof Promise ) {
186+
loader.file.then( file => {
187+
this._loadersMap.set( file, loader );
188+
} );
189+
}
181190

182191
loader.on( 'change:uploaded', () => {
183192
let aggregatedUploaded = 0;
@@ -207,15 +216,21 @@ export default class FileRepository extends Plugin {
207216
/**
208217
* Destroys the given loader.
209218
*
210-
* @param {File|module:upload/filerepository~FileLoader} fileOrLoader File associated with that loader or loader
211-
* itself.
219+
* @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
220+
* with that loader or loader itself.
212221
*/
213-
destroyLoader( fileOrLoader ) {
214-
const loader = fileOrLoader instanceof FileLoader ? fileOrLoader : this.getLoader( fileOrLoader );
222+
destroyLoader( fileOrPromiseOrLoader ) {
223+
const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader( fileOrPromiseOrLoader );
215224

216225
loader._destroy();
217226

218227
this.loaders.remove( loader );
228+
229+
this._loadersMap.forEach( ( value, key ) => {
230+
if ( value === loader ) {
231+
this._loadersMap.delete( key );
232+
}
233+
} );
219234
}
220235

221236
/**
@@ -252,10 +267,10 @@ class FileLoader {
252267
/**
253268
* Creates a new instance of `FileLoader`.
254269
*
255-
* @param {File} file A native file instance.
256-
* @param {module:upload/filerepository~UploadAdapter} adapter
270+
* @param {Promise.<File>} filePromise A promise which resolves to a file instance.
271+
* @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
257272
*/
258-
constructor( file, adapter ) {
273+
constructor( filePromise, uploadAdapterCreator ) {
259274
/**
260275
* Unique id of FileLoader instance.
261276
*
@@ -265,20 +280,20 @@ class FileLoader {
265280
this.id = uid();
266281

267282
/**
268-
* A `File` instance associated with this file loader.
283+
* Additional wrapper over the initial file promise passed to this loader.
269284
*
270-
* @readonly
271-
* @member {File}
285+
* @private
286+
* @member {module:upload/filerepository~FilePromiseWrapper}
272287
*/
273-
this.file = file;
288+
this._filePromiseWrapper = this._createFilePromiseWrapper( filePromise );
274289

275290
/**
276291
* Adapter instance associated with this file loader.
277292
*
278293
* @private
279294
* @member {module:upload/filerepository~UploadAdapter}
280295
*/
281-
this._adapter = adapter;
296+
this._adapter = uploadAdapterCreator( this );
282297

283298
/**
284299
* FileReader used by FileLoader.
@@ -354,6 +369,28 @@ class FileLoader {
354369
this.set( 'uploadResponse', null );
355370
}
356371

372+
/**
373+
* A `Promise` which resolves to a `File` instance associated with this file loader.
374+
*
375+
* @type {Promise.<File|null>}
376+
*/
377+
get file() {
378+
if ( !this._filePromiseWrapper ) {
379+
// Loader was destroyed, return promise which resolves to null.
380+
return Promise.resolve( null );
381+
} else {
382+
// The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
383+
//
384+
// * The `loader.file.then( ... )` is called by external code (returned promise is pending).
385+
// * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
386+
// * Promise returned by the first `loader.file.then( ... )` call is resolved.
387+
//
388+
// Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
389+
// is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
390+
return this._filePromiseWrapper.promise.then( file => this._filePromiseWrapper ? file : null );
391+
}
392+
}
393+
357394
/**
358395
* Reads file using {@link module:upload/filereader~FileReader}.
359396
*
@@ -372,7 +409,7 @@ class FileLoader {
372409
* }
373410
* } );
374411
*
375-
* @returns {Promise} Returns promise that will be resolved with read data. Promise will be rejected if error
412+
* @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
376413
* occurs or if read process is aborted.
377414
*/
378415
read() {
@@ -382,7 +419,8 @@ class FileLoader {
382419

383420
this.status = 'reading';
384421

385-
return this._reader.read( this.file )
422+
return this._filePromiseWrapper.promise
423+
.then( file => this._reader.read( file ) )
386424
.then( data => {
387425
this.status = 'idle';
388426

@@ -395,7 +433,7 @@ class FileLoader {
395433
}
396434

397435
this.status = 'error';
398-
throw this._reader.error;
436+
throw this._reader.error ? this._reader.error : err;
399437
} );
400438
}
401439

@@ -416,7 +454,7 @@ class FileLoader {
416454
* }
417455
* } );
418456
*
419-
* @returns {Promise} Returns promise that will be resolved with response data. Promise will be rejected if error
457+
* @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
420458
* occurs or if read process is aborted.
421459
*/
422460
upload() {
@@ -426,7 +464,8 @@ class FileLoader {
426464

427465
this.status = 'uploading';
428466

429-
return this._adapter.upload()
467+
return this._filePromiseWrapper.promise
468+
.then( () => this._adapter.upload() )
430469
.then( data => {
431470
this.uploadResponse = data;
432471
this.status = 'idle';
@@ -450,11 +489,11 @@ class FileLoader {
450489
const status = this.status;
451490
this.status = 'aborted';
452491

453-
if ( status == 'reading' ) {
492+
if ( !this._filePromiseWrapper.isFulfilled ) {
493+
this._filePromiseWrapper.rejecter( 'aborted' );
494+
} else if ( status == 'reading' ) {
454495
this._reader.abort();
455-
}
456-
457-
if ( status == 'uploading' && this._adapter.abort ) {
496+
} else if ( status == 'uploading' && this._adapter.abort ) {
458497
this._adapter.abort();
459498
}
460499

@@ -467,11 +506,41 @@ class FileLoader {
467506
* @private
468507
*/
469508
_destroy() {
509+
this._filePromiseWrapper = undefined;
470510
this._reader = undefined;
471511
this._adapter = undefined;
472512
this.data = undefined;
473513
this.uploadResponse = undefined;
474-
this.file = undefined;
514+
}
515+
516+
/**
517+
* Wraps a given file promise into another promise giving additional
518+
* control (resolving, rejecting, checking if fulfilled) over it.
519+
*
520+
* @private
521+
* @param filePromise The initial file promise to be wrapped.
522+
* @returns {module:upload/filerepository~FilePromiseWrapper}
523+
*/
524+
_createFilePromiseWrapper( filePromise ) {
525+
const wrapper = {};
526+
527+
wrapper.promise = new Promise( ( resolve, reject ) => {
528+
wrapper.resolver = resolve;
529+
wrapper.rejecter = reject;
530+
wrapper.isFulfilled = false;
531+
532+
filePromise
533+
.then( file => {
534+
wrapper.isFulfilled = true;
535+
resolve( file );
536+
} )
537+
.catch( err => {
538+
wrapper.isFulfilled = true;
539+
reject( err );
540+
} );
541+
} );
542+
543+
return wrapper;
475544
}
476545
}
477546

@@ -515,7 +584,7 @@ mix( FileLoader, ObservableMixin );
515584
* {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
516585
*
517586
* @method module:upload/filerepository~UploadAdapter#upload
518-
* @returns {Promise} Promise that should be resolved when data is uploaded.
587+
* @returns {Promise.<Object>} Promise that should be resolved when data is uploaded.
519588
*/
520589

521590
/**
@@ -527,3 +596,14 @@ mix( FileLoader, ObservableMixin );
527596
*
528597
* @method module:upload/filerepository~UploadAdapter#abort
529598
*/
599+
600+
/**
601+
* Object returned by {@link module:upload/filerepository~FileLoader#_createFilePromiseWrapper} method
602+
* to add more control over the initial file promise passed to {@link module:upload/filerepository~FileLoader}.
603+
*
604+
* @typedef {Object} module:upload/filerepository~FilePromiseWrapper
605+
* @property {Promise.<File>} promise Wrapper promise which can be chained for further processing.
606+
* @property {Function} resolver Resolves the promise when called.
607+
* @property {Function} rejecter Rejects the promise when called.
608+
* @property {Boolean} isFulfilled Whether original promise is already fulfilled.
609+
*/

0 commit comments

Comments
 (0)