Skip to content

Commit b0d603f

Browse files
drzrafilessiiviRaphaël Droz
authored
Convert codebase to asynchronous code (reroll v2) (#363)
* Remove the synchronous functions and convert upload() and uploadNextChunk() (and upper-level resume(), pause(), cancel(), removeFile(), and retry()) to async * Remove the need to call readFinished() from the `readFileFn` (only returns the data) * Make FlowFile async-first and use a couple of Jasmine expectAsync() * Maintain event flow consistency * tests: Convert tests into async/await * tests: added a test about async file-added hook * doc: Adapt documentation Co-authored-by: ilessiivi <[email protected]> Co-authored-by: Raphaël Droz <[email protected]>
1 parent d861737 commit b0d603f

13 files changed

+257
-468
lines changed

README.md

+17-33
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function, it will be passed a FlowFile, a FlowChunk and isTest boolean (Default:
126126
* `testChunks` Make a GET request to the server for each chunks to see if it already exists. If implemented on the server-side, this will allow for upload resumes even after a browser crash or even a computer restart. (Default: `true`)
127127
* `preprocess` Optional function to process each chunk before testing & sending. To the function it will be passed the chunk as parameter, and should call the `preprocessFinished` method on the chunk when finished. (Default: `null`)
128128
* `changeRawDataBeforeSend` Optional function to change Raw Data just before the XHR Request can be sent for each chunk. To the function, it will be passed the chunk and the data as a Parameter. Return the data which will be then sent to the XHR request without further modification. (Default: `null`). This is helpful when using FlowJS with [Google Cloud Storage](https://cloud.google.com/storage/docs/json_api/v1/how-tos/multipart-upload). Usage example can be seen [#276](https://github.com/flowjs/flow.js/pull/276). (For more, check issue [#170](https://github.com/flowjs/flow.js/issues/170)).
129-
* `initFileFn` Optional function to initialize the fileObject. To the function it will be passed a FlowFile and a FlowChunk arguments.
129+
* `initFileFn` Optional (asynchronous) function to initialize the fileObject. To the function it will be passed a FlowFile and a FlowChunk arguments.
130130
* `readFileFn` Optional function wrapping reading operation from the original file. To the function it will be passed the FlowFile, the startByte and endByte, the fileType and the FlowChunk.
131131
* `generateUniqueIdentifier` Override the function that generates unique identifiers for each file. (Default: `null`)
132132
* `maxChunkRetries` The maximum number of retries for a chunk before the upload is failed. Valid values are any positive integer and `undefined` for no limit. (Default: `0`)
@@ -167,24 +167,18 @@ parameter must be adjusted together with `progressCallbacksInterval` parameter.
167167
* `.off()` All events are removed.
168168
* `.off(event)` Remove all callbacks of specific event.
169169
* `.off(event, callback)` Remove specific callback of event. `callback` should be a `Function`.
170-
* `.upload()` Start or resume uploading.
171-
* `.pause()` Pause uploading.
172-
* `.resume()` Resume uploading.
173-
* `.cancel()` Cancel upload of all `FlowFile` objects and remove them from the list.
170+
* `.upload()` [async] Start or resume uploading.
171+
* `.pause()` [async] Pause uploading.
172+
* `.resume()` [async] Resume uploading.
173+
* `.cancel()` [asyc] Cancel upload of all `FlowFile` objects and remove them from the list.
174174
* `.progress()` Returns a float between 0 and 1 indicating the current upload progress of all files.
175175
* `.isUploading()` Returns a boolean indicating whether or not the instance is currently uploading anything.
176-
* `.addFile(file, event = null, initFileFn = undefined)` Add a HTML5 File object to the list of files.
176+
* `.addFile(file, event = null, initFileFn = undefined)` [async] Add a HTML5 File object to the list of files.
177177
* Accept the same `event` and `initFileFn` parameters thant `addFiles` which is used under the hood.
178-
* `.addFiles([files], event = null, initFileFn = undefined)` Add multiple File objects to the list of files.
178+
* `.addFiles([files], event = null, initFileFn = undefined)` [async] Add multiple File objects to the list of files and returns the promise of the corresponding FlowFiles.
179179
* `event` The optional event that trigger the addition (for internal purposes)
180-
* `initFileFn` An override of Flow.initFileFn
181-
* `.asyncAddFile(file, event = null, initFileFn = undefined)` Add a HTML5 File object to the list of files.
182-
* `.asyncAddFiles([files], event = null, initFileFn = undefined)` Add multiple File objects to the list of files.
183-
* `asyncAddFile` and `asyncAddFiles` rely on the same parameters than they non-async counterparts with one
184-
difference: They accept an asynchronous `initFileFn` file function and return, in a promise, the corresponding FlowFiles.
185-
* Note: Calling `asyncAddFile` or `asyncAddFiles` with no `initFileFn` being defined is aimed identical to there non-async
186-
counterpart but this may change in the future [TBD].
187-
* `.removeFile(file)` Cancel upload of a specific `FlowFile` object on the list from the list.
180+
* `initFileFn` An [async] override of Flow.initFileFn
181+
* `.removeFile(file)` [asyc] Cancel upload of a specific `FlowFile` object on the list from the list.
188182
* `.getFromUniqueIdentifier(uniqueIdentifier)` Look up a `FlowFile` object by its unique identifier.
189183
* `.getSize()` Returns the total size of the upload in bytes.
190184
* `.sizeUploaded()` Returns the total size uploaded of all files in bytes.
@@ -207,27 +201,16 @@ Events are native, synchronous and provide information about the internal state
207201

208202
#### Processing hooks
209203

210-
Hooks allows for either synchronous or asynchronous operations and allow altering the regular processing of the file(s) from addition to upload completion.
211-
It's user responsability to use or not the `async` version of `(async)?addFile` and `(async)?addFiles` according to the behavior of its processing hooks.
212-
(Defining `async` callbacks for the `asyncAddFile(s)`)
213-
204+
Hooks allows for either (possibly asynchronous) operations and allow altering the regular processing of the file(s) from addition to upload completion.
214205
* `file-added(<FlowFile> file, event) : null` This event is also called before file is added to upload queue and after it's been fully initialized. `event` is the browser `event` object from when the file was added.
215206
* `files-added([<FlowFile> files], event) : null` Same as `file-added`, but used for multiple file validation.
216207
* `files-submitted([<FlowFile> files], event) : null` Same as `files-added`, but happens after the file is added to upload queue. Can be used to start upload of currently added files.
217208
* `filter-file(<FlowFile> file, event) : boolean` The boolean return value decide whether this particular file must be processed or ignored.
218209

219210
### Hooks and events format
220-
- Events and hooks name are case-sensitive, snake-cased and return CustomEvent passed straight to `Flow.on()` callback.
211+
- Events and hooks name are case-sensitive, snake-cased.
212+
- In the case of events, a CustomEvent passed straight to callback passed to `Flow.on()`.
221213
- Sample use `flow.on('file-removed', ({detail: [file]}) => { ... });`
222-
- In an attempt of backward compatibility, some support of camelCase events exist:
223-
```
224-
flow.on('filesAdded', async (files, event) => { // v2 events prototype
225-
if (files instanceof CustomEvent) { // Handle v3+ events
226-
var [files, event] = files.detail;
227-
}
228-
// Do something with files
229-
});
230-
```
231214

232215
### FlowFile
233216
FlowFile constructor can be accessed in `Flow.FlowFile`.
@@ -250,11 +233,12 @@ FlowFile constructor can be accessed in `Flow.FlowFile`.
250233

251234
* `.progress(relative)` Returns a float between 0 and 1 indicating the current upload progress of the file. If `relative` is `true`, the value is returned relative to all files in the Flow.js instance.
252235
* `.pause()` Pause uploading the file.
253-
* `.resume()` Resume uploading the file.
254-
* `.cancel()` Abort uploading the file and delete it from the list of files to upload.
255-
* `.retry()` Retry uploading the file.
256-
* `.bootstrap()` Rebuild the state of a `FlowFile` object, including reassigning chunks and XMLHttpRequest instances.
236+
* `.resume()` [async] Resume uploading the file.
237+
* `.cancel()` [async] Abort uploading the file and delete it from the list of files to upload.
238+
* `.retry()` [async] Retry uploading the file.
239+
* `.bootstrap()` [async / internal use only] Rebuild the state of a `FlowFile` object, including reassigning chunks and XMLHttpRequest instances.
257240
* `.isUploading()` Returns a boolean indicating whether file chunks is uploading.
241+
* `.isReading()` Returns a boolean indicating whether the file/stream is being read.
258242
* `.isComplete()` Returns a boolean indicating whether the file has completed uploading and received a server response.
259243
* `.sizeUploaded()` Returns size uploaded in bytes.
260244
* `.timeRemaining()` Returns remaining time to finish upload file in seconds. Accuracy is based on average speed. If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`

src/AsyncFlowFile.js

-47
This file was deleted.

src/Eventizer.js

+17-59
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* Hooks exist to allow users to alter flow.js file processing. This is intended for users relying on a dropzone and other higher-level components
77
* when the flow between `addFiles()` and `upload()` is hardly configurable.
8-
* Users calling `await flow.asyncAddFiles()` have more room for customization before calling `upload();` without having to rely upon hooks.
8+
* Users calling `await flow.addFiles()` have more room for customization before calling `upload();` without having to rely upon hooks.
99
*
1010
* Hooks can *alter* the parameters they receive (javascript pass-by-reference rules will apply).
1111
* For example, the `file-added` hook receives a `flowfile` parameter. `delete flowfile` or `flowfile = {}` have no effect
@@ -49,20 +49,18 @@ const EVENTS = [
4949
* This class:
5050
* - add EventListener support to an object.
5151
* - wrap EventListener attachment in order to ease their removal
52-
* - add the concept of processing hooks similar to native events (explained below)
52+
* - add the concept of processing hooks similar to native events (explained below)
5353
*
5454
* The file is organized in three parts:
5555
* 1. isHook, isFilter, isEvent, on, off
56-
* wrap the above concept and offer an unified interface. Whether a callback
57-
* apply to a hook or an event is determined by its name (and the "async" nature of
58-
* the callback).
56+
* wrap the above concept and offer a unified interface. Whether a callback
57+
* apply to a hook or an event is determined by its name.
5958
*
6059
* 2. Events: addEventListener, removeEventListener and *emit()*
6160
* apply to addition/removal/dispatching of *events*.
6261
*
6362
* 3. Hooks: addHook, hasHook, removeHook apply to addition/removal of *hooks*.
6463
* - *hook()* trigger the hook execution.
65-
* - *aHook()* is the async counterpart.
6664
*/
6765

6866
EventTarget.prototype._addEventListener = EventTarget.prototype.addEventListener;
@@ -77,8 +75,7 @@ export default class extends EventTarget {
7775
*
7876
* Each key is check against an hardcoded to list to defined whether:
7977
* - it's a "native" CustomEvent (dispatched asynchronously dirsregarding its value)
80-
* - it's a known event (whether a "filter" or an "action", and in this case, whether
81-
* each callback is asynchronous or not.
78+
* - it's a known event (whether a "filter" or an "action")
8279
* @type {}
8380
*/
8481
constructor(hooks_events = {}) {
@@ -252,8 +249,7 @@ export default class extends EventTarget {
252249
* ### HOOKS ###
253250
*/
254251
addHook(event, callback, options) {
255-
var isAsync = callback.constructor.name === 'AsyncFunction',
256-
target = isAsync ? this._asyncHooks : this._hooks;
252+
var target = this._hooks;
257253
if (!target.hasOwnProperty(event)) {
258254
target[event] = [];
259255
}
@@ -262,7 +258,7 @@ export default class extends EventTarget {
262258

263259
hasHook(async, events) {
264260
events = typeof events === 'string' ? [events] : events || [];
265-
var target = async ? this._asyncHooks : this._hooks;
261+
var target = this._hooks;
266262
for (let [k, v] of Object.entries(target)) {
267263
if (events.length > 0 && ! events.includes(k)) {
268264
continue;
@@ -288,55 +284,18 @@ export default class extends EventTarget {
288284

289285
if (event && event != '*') {
290286
if (callback) {
291-
var isAsync = callback.constructor.name === 'AsyncFunction',
292-
target = isAsync ? this._asyncHooks : this._hooks;
287+
var target = this._hooks;
293288
if (target.hasOwnProperty(event)) {
294289
arrayRemove(target[event], callback);
295290
}
296291
} else {
297292
delete this._hooks[event];
298-
delete this._asyncHooks[event];
299293
}
300294
} else {
301295
this._hooks = {};
302-
this._asyncHooks = {};
303296
}
304297
}
305298

306-
/**
307-
* Run a synchronous hook (action or filter).
308-
*
309-
* @param {string} event event name
310-
* @param {...} args arguments of a callback
311-
*
312-
* @return {bool} In the case of *filters*, indicates whether processing must continue.
313-
* @return null In the case of *actions*.
314-
*/
315-
hook(name, ...args) {
316-
let value,
317-
preventDefault = false,
318-
isFilter = this.isFilter(name),
319-
callbacks = this._hooks[name] || [];
320-
321-
for (let callback of callbacks) {
322-
// console.log(`[event] Fire hook "${name}"${args.length ? ' with ' + args.length + ' arguments' : ''}`);
323-
value = callback.apply(this, args);
324-
if (name === 'file-added' && value === false) {
325-
console.warn('In Flow.js 3.x, file-added event is an action rather than a filter. Return value is ignored but removing the `file` property allows to skip an enqueued file.');
326-
}
327-
328-
if (isFilter) {
329-
// console.log(`[filter-event] ${event} returned:`, item.value);
330-
preventDefault |= (value === false);
331-
} else {
332-
// Changes happen by reference. We ignore iterator.next().value.
333-
}
334-
}
335-
336-
this.emitCatchAll(name, ...args);
337-
return isFilter ? !preventDefault : null;
338-
}
339-
340299
/**
341300
* Run an asynchronous hook (action or filter).
342301
*
@@ -346,19 +305,18 @@ export default class extends EventTarget {
346305
* @return {bool} In the case of *filters*, indicates whether processing must continue.
347306
* @return {mixed} In the case of *actions*: The first argument (possibly modified by hooks).
348307
*/
349-
async aHook(name, ...args) {
350-
const calls = this._asyncHooks[name] || [],
351-
isFilter = this.isFilter(name);
308+
async hook(name, ...args) {
309+
let calls = this._hooks[name] || [],
310+
isFilter = this.isFilter(name),
311+
returns = isFilter ? true : args[0];
352312

353-
if (! calls.length) {
354-
return isFilter ? true : args[0];
313+
if (calls.length) {
314+
// console.log(`[event] Fire ${calls.length} async hook for "${name}"${args.length ? ' with ' + args.length + ' arguments' : ''}`);
315+
const results = await Promise.all(calls.map(e => e.apply(this, args)));
316+
returns = isFilter ? !results.includes(false) : results;
355317
}
356318

357-
// console.log(`[event] Fire ${calls.length} async hook for "${name}"${args.length ? ' with ' + args.length + ' arguments' : ''}`);
358-
const returns = await Promise.all(calls.map(e => e.apply(this, args)));
359-
360319
this.emitCatchAll(name, ...args);
361-
362-
return isFilter ? !returns.includes(false) : returns;
320+
return returns;
363321
}
364322
}

0 commit comments

Comments
 (0)