Files
tubestation/addon-sdk/source/lib/sdk/io/stream.js

439 lines
14 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
"stability": "experimental"
};
const { CC, Cc, Ci, Cu, Cr, components } = require("chrome");
const { EventTarget } = require("../event/target");
const { emit } = require("../event/core");
const { Buffer } = require("./buffer");
const { Class } = require("../core/heritage");
const { setTimeout } = require("../timers");
const MultiplexInputStream = CC("@mozilla.org/io/multiplex-input-stream;1",
"nsIMultiplexInputStream");
const AsyncStreamCopier = CC("@mozilla.org/network/async-stream-copier;1",
"nsIAsyncStreamCopier", "init");
const StringInputStream = CC("@mozilla.org/io/string-input-stream;1",
"nsIStringInputStream");
const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1",
"nsIArrayBufferInputStream");
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
const InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1",
"nsIInputStreamPump", "init");
const threadManager = Cc["@mozilla.org/thread-manager;1"].
getService(Ci.nsIThreadManager);
const eventTarget = Cc["@mozilla.org/network/stream-transport-service;1"].
getService(Ci.nsIEventTarget);
var isFunction = value => typeof(value) === "function"
function accessor() {
let map = new WeakMap();
return function(target, value) {
if (value)
map.set(target, value);
return map.get(target);
}
}
const Stream = Class({
extends: EventTarget,
initialize: function() {
this.readable = false;
this.writable = false;
this.encoding = null;
},
setEncoding: function setEncoding(encoding) {
this.encoding = String(encoding).toUpperCase();
},
pipe: function pipe(target, options) {
let source = this;
function onData(chunk) {
if (target.writable) {
if (false === target.write(chunk))
source.pause();
}
}
function onDrain() {
if (source.readable)
source.resume();
}
function onEnd() {
target.end();
}
function onPause() {
source.pause();
}
function onResume() {
if (source.readable)
source.resume();
}
function cleanup() {
source.removeListener("data", onData);
target.removeListener("drain", onDrain);
source.removeListener("end", onEnd);
target.removeListener("pause", onPause);
target.removeListener("resume", onResume);
source.removeListener("end", cleanup);
source.removeListener("close", cleanup);
target.removeListener("end", cleanup);
target.removeListener("close", cleanup);
}
if (!options || options.end !== false)
target.on("end", onEnd);
source.on("data", onData);
target.on("drain", onDrain);
target.on("resume", onResume);
target.on("pause", onPause);
source.on("end", cleanup);
source.on("close", cleanup);
target.on("end", cleanup);
target.on("close", cleanup);
emit(target, "pipe", source);
},
pause: function pause() {
emit(this, "pause");
},
resume: function resume() {
emit(this, "resume");
},
destroySoon: function destroySoon() {
this.destroy();
}
});
exports.Stream = Stream;
var nsIStreamListener = accessor();
var nsIInputStreamPump = accessor();
var nsIAsyncInputStream = accessor();
var nsIBinaryInputStream = accessor();
const StreamListener = Class({
initialize: function(stream) {
this.stream = stream;
},
// Next three methods are part of `nsIStreamListener` interface and are
// invoked by `nsIInputStreamPump.asyncRead`.
onDataAvailable: function(request, context, input, offset, count) {
let stream = this.stream;
let buffer = new ArrayBuffer(count);
nsIBinaryInputStream(stream).readArrayBuffer(count, buffer);
emit(stream, "data", new Buffer(buffer));
},
// Next two methods implement `nsIRequestObserver` interface and are invoked
// by `nsIInputStreamPump.asyncRead`.
onStartRequest: function() {},
// Called to signify the end of an asynchronous request. We only care to
// discover errors.
onStopRequest: function(request, context, status) {
let stream = this.stream;
stream.readable = false;
if (!components.isSuccessCode(status))
emit(stream, "error", status);
else
emit(stream, "end");
}
});
const InputStream = Class({
extends: Stream,
readable: false,
paused: false,
initialize: function initialize(options) {
let { asyncInputStream } = options;
this.readable = true;
let binaryInputStream = new BinaryInputStream(asyncInputStream);
let inputStreamPump = new InputStreamPump(asyncInputStream,
-1, -1, 0, 0, false);
let streamListener = new StreamListener(this);
nsIAsyncInputStream(this, asyncInputStream);
nsIInputStreamPump(this, inputStreamPump);
nsIBinaryInputStream(this, binaryInputStream);
nsIStreamListener(this, streamListener);
this.asyncInputStream = asyncInputStream;
this.inputStreamPump = inputStreamPump;
this.binaryInputStream = binaryInputStream;
},
get status() nsIInputStreamPump(this).status,
read: function() {
nsIInputStreamPump(this).asyncRead(nsIStreamListener(this), null);
},
pause: function pause() {
this.paused = true;
nsIInputStreamPump(this).suspend();
emit(this, "paused");
},
resume: function resume() {
this.paused = false;
if (nsIInputStreamPump(this).isPending()) {
nsIInputStreamPump(this).resume();
emit(this, "resume");
}
},
close: function close() {
this.readable = false;
nsIInputStreamPump(this).cancel(Cr.NS_OK);
nsIBinaryInputStream(this).close();
nsIAsyncInputStream(this).close();
},
destroy: function destroy() {
this.close();
nsIInputStreamPump(this);
nsIAsyncInputStream(this);
nsIBinaryInputStream(this);
nsIStreamListener(this);
}
});
exports.InputStream = InputStream;
var nsIRequestObserver = accessor();
var nsIAsyncOutputStream = accessor();
var nsIAsyncStreamCopier = accessor();
var nsIMultiplexInputStream = accessor();
const RequestObserver = Class({
initialize: function(stream) {
this.stream = stream;
},
// Method is part of `nsIRequestObserver` interface that is
// invoked by `nsIAsyncStreamCopier.asyncCopy`.
onStartRequest: function() {},
// Method is part of `nsIRequestObserver` interface that is
// invoked by `nsIAsyncStreamCopier.asyncCopy`.
onStopRequest: function(request, context, status) {
let stream = this.stream;
stream.drained = true;
// Remove copied chunk.
let multiplexInputStream = nsIMultiplexInputStream(stream);
multiplexInputStream.removeStream(0);
// If there was an error report.
if (!components.isSuccessCode(status))
emit(stream, "error", status);
// If there more chunks in queue then flush them.
else if (multiplexInputStream.count)
stream.flush();
// If stream is still writable notify that queue has drained.
else if (stream.writable)
emit(stream, "drain");
// If stream is no longer writable close it.
else {
nsIAsyncStreamCopier(stream).cancel(Cr.NS_OK);
nsIMultiplexInputStream(stream).close();
nsIAsyncOutputStream(stream).close();
nsIAsyncOutputStream(stream).flush();
}
}
});
const OutputStreamCallback = Class({
initialize: function(stream) {
this.stream = stream;
},
// Method is part of `nsIOutputStreamCallback` interface that
// is invoked by `nsIAsyncOutputStream.asyncWait`. It is registered
// with `WAIT_CLOSURE_ONLY` flag that overrides the default behavior,
// causing the `onOutputStreamReady` notification to be suppressed until
// the stream becomes closed.
onOutputStreamReady: function(nsIAsyncOutputStream) {
emit(this.stream, "finish");
}
});
const OutputStream = Class({
extends: Stream,
writable: false,
drained: true,
get bufferSize() {
let multiplexInputStream = nsIMultiplexInputStream(this);
return multiplexInputStream && multiplexInputStream.available();
},
initialize: function initialize(options) {
let { asyncOutputStream, output } = options;
this.writable = true;
// Ensure that `nsIAsyncOutputStream` was provided.
asyncOutputStream.QueryInterface(Ci.nsIAsyncOutputStream);
// Create a `nsIMultiplexInputStream` and `nsIAsyncStreamCopier`. Former
// is used to queue written data chunks that `asyncStreamCopier` will
// asynchronously drain into `asyncOutputStream`.
let multiplexInputStream = MultiplexInputStream();
let asyncStreamCopier = AsyncStreamCopier(multiplexInputStream,
output || asyncOutputStream,
eventTarget,
// nsIMultiplexInputStream
// implemnts .readSegments()
true,
// nsIOutputStream may or
// may not implemnet
// .writeSegments().
false,
// Use default buffer size.
null,
// Should not close an input.
false,
// Should not close an output.
false);
// Create `requestObserver` implementing `nsIRequestObserver` interface
// in the constructor that's gonna be reused across several flushes.
let requestObserver = RequestObserver(this);
// Create observer that implements `nsIOutputStreamCallback` and register
// using `WAIT_CLOSURE_ONLY` flag. That way it will be notfied once
// `nsIAsyncOutputStream` is closed.
asyncOutputStream.asyncWait(OutputStreamCallback(this),
asyncOutputStream.WAIT_CLOSURE_ONLY,
0,
threadManager.currentThread);
nsIRequestObserver(this, requestObserver);
nsIAsyncOutputStream(this, asyncOutputStream);
nsIMultiplexInputStream(this, multiplexInputStream);
nsIAsyncStreamCopier(this, asyncStreamCopier);
this.asyncOutputStream = asyncOutputStream;
this.multiplexInputStream = multiplexInputStream;
this.asyncStreamCopier = asyncStreamCopier;
},
write: function write(content, encoding, callback) {
if (isFunction(encoding)) {
callback = encoding;
encoding = callback;
}
// If stream is not writable we throw an error.
if (!this.writable) throw Error("stream is not writable");
let chunk = null;
// If content is not a buffer then we create one out of it.
if (Buffer.isBuffer(content)) {
chunk = new ArrayBufferInputStream();
chunk.setData(content.buffer, 0, content.length);
}
else {
chunk = new StringInputStream();
chunk.setData(content, content.length);
}
if (callback)
this.once("drain", callback);
// Queue up chunk to be copied to output sync.
nsIMultiplexInputStream(this).appendStream(chunk);
this.flush();
return this.drained;
},
flush: function() {
if (this.drained) {
this.drained = false;
nsIAsyncStreamCopier(this).asyncCopy(nsIRequestObserver(this), null);
}
},
end: function end(content, encoding, callback) {
if (isFunction(content)) {
callback = content
content = callback
}
if (isFunction(encoding)) {
callback = encoding
encoding = callback
}
// Setting a listener to "finish" event if passed.
if (isFunction(callback))
this.once("finish", callback);
if (content)
this.write(content, encoding);
this.writable = false;
// Close `asyncOutputStream` only if output has drained. If it's
// not drained than `asyncStreamCopier` is busy writing, so let
// it finish. Note that since `this.writable` is false copier will
// close `asyncOutputStream` once output drains.
if (this.drained)
nsIAsyncOutputStream(this).close();
},
destroy: function destroy() {
nsIAsyncOutputStream(this).close();
nsIAsyncOutputStream(this);
nsIMultiplexInputStream(this);
nsIAsyncStreamCopier(this);
nsIRequestObserver(this);
}
});
exports.OutputStream = OutputStream;
const DuplexStream = Class({
extends: Stream,
implements: [InputStream, OutputStream],
allowHalfOpen: true,
initialize: function initialize(options) {
options = options || {};
let { readable, writable, allowHalfOpen } = options;
InputStream.prototype.initialize.call(this, options);
OutputStream.prototype.initialize.call(this, options);
if (readable === false)
this.readable = false;
if (writable === false)
this.writable = false;
if (allowHalfOpen === false)
this.allowHalfOpen = false;
// If in a half open state and it's disabled enforce end.
this.once("end", () => {
if (!this.allowHalfOpen && (!this.readable || !this.writable))
this.end();
});
},
destroy: function destroy(error) {
InputStream.prototype.destroy.call(this);
OutputStream.prototype.destroy.call(this);
}
});
exports.DuplexStream = DuplexStream;