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;
|