264 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| "use strict";
 | |
| 
 | |
| /**
 | |
|  * A worker that does nothing but passing chunks to the next one. This is like
 | |
|  * a nodejs stream but with some differences. On the good side :
 | |
|  * - it works on IE 6-9 without any issue / polyfill
 | |
|  * - it weights less than the full dependencies bundled with browserify
 | |
|  * - it forwards errors (no need to declare an error handler EVERYWHERE)
 | |
|  *
 | |
|  * A chunk is an object with 2 attributes : `meta` and `data`. The former is an
 | |
|  * object containing anything (`percent` for example), see each worker for more
 | |
|  * details. The latter is the real data (String, Uint8Array, etc).
 | |
|  *
 | |
|  * @constructor
 | |
|  * @param {String} name the name of the stream (mainly used for debugging purposes)
 | |
|  */
 | |
| function GenericWorker(name) {
 | |
|     // the name of the worker
 | |
|     this.name = name || "default";
 | |
|     // an object containing metadata about the workers chain
 | |
|     this.streamInfo = {};
 | |
|     // an error which happened when the worker was paused
 | |
|     this.generatedError = null;
 | |
|     // an object containing metadata to be merged by this worker into the general metadata
 | |
|     this.extraStreamInfo = {};
 | |
|     // true if the stream is paused (and should not do anything), false otherwise
 | |
|     this.isPaused = true;
 | |
|     // true if the stream is finished (and should not do anything), false otherwise
 | |
|     this.isFinished = false;
 | |
|     // true if the stream is locked to prevent further structure updates (pipe), false otherwise
 | |
|     this.isLocked = false;
 | |
|     // the event listeners
 | |
|     this._listeners = {
 | |
|         "data":[],
 | |
|         "end":[],
 | |
|         "error":[]
 | |
|     };
 | |
|     // the previous worker, if any
 | |
|     this.previous = null;
 | |
| }
 | |
| 
 | |
| GenericWorker.prototype = {
 | |
|     /**
 | |
|      * Push a chunk to the next workers.
 | |
|      * @param {Object} chunk the chunk to push
 | |
|      */
 | |
|     push : function (chunk) {
 | |
|         this.emit("data", chunk);
 | |
|     },
 | |
|     /**
 | |
|      * End the stream.
 | |
|      * @return {Boolean} true if this call ended the worker, false otherwise.
 | |
|      */
 | |
|     end : function () {
 | |
|         if (this.isFinished) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         this.flush();
 | |
|         try {
 | |
|             this.emit("end");
 | |
|             this.cleanUp();
 | |
|             this.isFinished = true;
 | |
|         } catch (e) {
 | |
|             this.emit("error", e);
 | |
|         }
 | |
|         return true;
 | |
|     },
 | |
|     /**
 | |
|      * End the stream with an error.
 | |
|      * @param {Error} e the error which caused the premature end.
 | |
|      * @return {Boolean} true if this call ended the worker with an error, false otherwise.
 | |
|      */
 | |
|     error : function (e) {
 | |
|         if (this.isFinished) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if(this.isPaused) {
 | |
|             this.generatedError = e;
 | |
|         } else {
 | |
|             this.isFinished = true;
 | |
| 
 | |
|             this.emit("error", e);
 | |
| 
 | |
|             // in the workers chain exploded in the middle of the chain,
 | |
|             // the error event will go downward but we also need to notify
 | |
|             // workers upward that there has been an error.
 | |
|             if(this.previous) {
 | |
|                 this.previous.error(e);
 | |
|             }
 | |
| 
 | |
|             this.cleanUp();
 | |
|         }
 | |
|         return true;
 | |
|     },
 | |
|     /**
 | |
|      * Add a callback on an event.
 | |
|      * @param {String} name the name of the event (data, end, error)
 | |
|      * @param {Function} listener the function to call when the event is triggered
 | |
|      * @return {GenericWorker} the current object for chainability
 | |
|      */
 | |
|     on : function (name, listener) {
 | |
|         this._listeners[name].push(listener);
 | |
|         return this;
 | |
|     },
 | |
|     /**
 | |
|      * Clean any references when a worker is ending.
 | |
|      */
 | |
|     cleanUp : function () {
 | |
|         this.streamInfo = this.generatedError = this.extraStreamInfo = null;
 | |
|         this._listeners = [];
 | |
|     },
 | |
|     /**
 | |
|      * Trigger an event. This will call registered callback with the provided arg.
 | |
|      * @param {String} name the name of the event (data, end, error)
 | |
|      * @param {Object} arg the argument to call the callback with.
 | |
|      */
 | |
|     emit : function (name, arg) {
 | |
|         if (this._listeners[name]) {
 | |
|             for(var i = 0; i < this._listeners[name].length; i++) {
 | |
|                 this._listeners[name][i].call(this, arg);
 | |
|             }
 | |
|         }
 | |
|     },
 | |
|     /**
 | |
|      * Chain a worker with an other.
 | |
|      * @param {Worker} next the worker receiving events from the current one.
 | |
|      * @return {worker} the next worker for chainability
 | |
|      */
 | |
|     pipe : function (next) {
 | |
|         return next.registerPrevious(this);
 | |
|     },
 | |
|     /**
 | |
|      * Same as `pipe` in the other direction.
 | |
|      * Using an API with `pipe(next)` is very easy.
 | |
|      * Implementing the API with the point of view of the next one registering
 | |
|      * a source is easier, see the ZipFileWorker.
 | |
|      * @param {Worker} previous the previous worker, sending events to this one
 | |
|      * @return {Worker} the current worker for chainability
 | |
|      */
 | |
|     registerPrevious : function (previous) {
 | |
|         if (this.isLocked) {
 | |
|             throw new Error("The stream '" + this + "' has already been used.");
 | |
|         }
 | |
| 
 | |
|         // sharing the streamInfo...
 | |
|         this.streamInfo = previous.streamInfo;
 | |
|         // ... and adding our own bits
 | |
|         this.mergeStreamInfo();
 | |
|         this.previous =  previous;
 | |
|         var self = this;
 | |
|         previous.on("data", function (chunk) {
 | |
|             self.processChunk(chunk);
 | |
|         });
 | |
|         previous.on("end", function () {
 | |
|             self.end();
 | |
|         });
 | |
|         previous.on("error", function (e) {
 | |
|             self.error(e);
 | |
|         });
 | |
|         return this;
 | |
|     },
 | |
|     /**
 | |
|      * Pause the stream so it doesn't send events anymore.
 | |
|      * @return {Boolean} true if this call paused the worker, false otherwise.
 | |
|      */
 | |
|     pause : function () {
 | |
|         if(this.isPaused || this.isFinished) {
 | |
|             return false;
 | |
|         }
 | |
|         this.isPaused = true;
 | |
| 
 | |
|         if(this.previous) {
 | |
|             this.previous.pause();
 | |
|         }
 | |
|         return true;
 | |
|     },
 | |
|     /**
 | |
|      * Resume a paused stream.
 | |
|      * @return {Boolean} true if this call resumed the worker, false otherwise.
 | |
|      */
 | |
|     resume : function () {
 | |
|         if(!this.isPaused || this.isFinished) {
 | |
|             return false;
 | |
|         }
 | |
|         this.isPaused = false;
 | |
| 
 | |
|         // if true, the worker tried to resume but failed
 | |
|         var withError = false;
 | |
|         if(this.generatedError) {
 | |
|             this.error(this.generatedError);
 | |
|             withError = true;
 | |
|         }
 | |
|         if(this.previous) {
 | |
|             this.previous.resume();
 | |
|         }
 | |
| 
 | |
|         return !withError;
 | |
|     },
 | |
|     /**
 | |
|      * Flush any remaining bytes as the stream is ending.
 | |
|      */
 | |
|     flush : function () {},
 | |
|     /**
 | |
|      * Process a chunk. This is usually the method overridden.
 | |
|      * @param {Object} chunk the chunk to process.
 | |
|      */
 | |
|     processChunk : function(chunk) {
 | |
|         this.push(chunk);
 | |
|     },
 | |
|     /**
 | |
|      * Add a key/value to be added in the workers chain streamInfo once activated.
 | |
|      * @param {String} key the key to use
 | |
|      * @param {Object} value the associated value
 | |
|      * @return {Worker} the current worker for chainability
 | |
|      */
 | |
|     withStreamInfo : function (key, value) {
 | |
|         this.extraStreamInfo[key] = value;
 | |
|         this.mergeStreamInfo();
 | |
|         return this;
 | |
|     },
 | |
|     /**
 | |
|      * Merge this worker's streamInfo into the chain's streamInfo.
 | |
|      */
 | |
|     mergeStreamInfo : function () {
 | |
|         for(var key in this.extraStreamInfo) {
 | |
|             if (!Object.prototype.hasOwnProperty.call(this.extraStreamInfo, key)) {
 | |
|                 continue;
 | |
|             }
 | |
|             this.streamInfo[key] = this.extraStreamInfo[key];
 | |
|         }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Lock the stream to prevent further updates on the workers chain.
 | |
|      * After calling this method, all calls to pipe will fail.
 | |
|      */
 | |
|     lock: function () {
 | |
|         if (this.isLocked) {
 | |
|             throw new Error("The stream '" + this + "' has already been used.");
 | |
|         }
 | |
|         this.isLocked = true;
 | |
|         if (this.previous) {
 | |
|             this.previous.lock();
 | |
|         }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      *
 | |
|      * Pretty print the workers chain.
 | |
|      */
 | |
|     toString : function () {
 | |
|         var me = "Worker " + this.name;
 | |
|         if (this.previous) {
 | |
|             return this.previous + " -> " + me;
 | |
|         } else {
 | |
|             return me;
 | |
|         }
 | |
|     }
 | |
| };
 | |
| 
 | |
| module.exports = GenericWorker;
 |