UNPKG

6.25 kBJavaScriptView Raw
1// a transform stream is a readable/writable stream where you do
2// something with the data. Sometimes it's called a "filter",
3// but that's not a great name for it, since that implies a thing where
4// some bits pass through, and others are simply ignored. (That would
5// be a valid example of a transform, of course.)
6//
7// While the output is causally related to the input, it's not a
8// necessarily symmetric or synchronous transformation. For example,
9// a zlib stream might take multiple plain-text writes(), and then
10// emit a single compressed chunk some time in the future.
11//
12// Here's how this works:
13//
14// The Transform stream has all the aspects of the readable and writable
15// stream classes. When you write(chunk), that calls _write(chunk,cb)
16// internally, and returns false if there's a lot of pending writes
17// buffered up. When you call read(), that calls _read(n) until
18// there's enough pending readable data buffered up.
19//
20// In a transform stream, the written data is placed in a buffer. When
21// _read(n) is called, it transforms the queued up data, calling the
22// buffered _write cb's as it consumes chunks. If consuming a single
23// written chunk would result in multiple output chunks, then the first
24// outputted bit calls the readcb, and subsequent chunks just go into
25// the read buffer, and will cause it to emit 'readable' if necessary.
26//
27// This way, back-pressure is actually determined by the reading side,
28// since _read has to be called to start processing a new chunk. However,
29// a pathological inflate type of transform can cause excessive buffering
30// here. For example, imagine a stream where every byte of input is
31// interpreted as an integer from 0-255, and then results in that many
32// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in
33// 1kb of data being output. In this case, you could write a very small
34// amount of input, and end up with a very large amount of output. In
35// such a pathological inflating mechanism, there'd be no way to tell
36// the system to stop doing the transform. A single 4MB write could
37// cause the system to run out of memory.
38//
39// However, even in such a pathological case, only a single written chunk
40// would be consumed, and then the rest would wait (un-transformed) until
41// the results of the previous transformed chunk were consumed.
42
43
44import {Duplex} from './duplex';
45
46
47import {inherits} from 'util';
48inherits(Transform, Duplex);
49
50function TransformState(stream) {
51 this.afterTransform = function (er, data) {
52 return afterTransform(stream, er, data);
53 };
54
55 this.needTransform = false;
56 this.transforming = false;
57 this.writecb = null;
58 this.writechunk = null;
59 this.writeencoding = null;
60}
61
62function afterTransform(stream, er, data) {
63 var ts = stream._transformState;
64 ts.transforming = false;
65
66 var cb = ts.writecb;
67
68 if (!cb) return stream.emit('error', new Error('no writecb in Transform class'));
69
70 ts.writechunk = null;
71 ts.writecb = null;
72
73 if (data !== null && data !== undefined) stream.push(data);
74
75 cb(er);
76
77 var rs = stream._readableState;
78 rs.reading = false;
79 if (rs.needReadable || rs.length < rs.highWaterMark) {
80 stream._read(rs.highWaterMark);
81 }
82}
83export default Transform;
84export function Transform(options) {
85 if (!(this instanceof Transform)) return new Transform(options);
86
87 Duplex.call(this, options);
88
89 this._transformState = new TransformState(this);
90
91 // when the writable side finishes, then flush out anything remaining.
92 var stream = this;
93
94 // start out asking for a readable event once data is transformed.
95 this._readableState.needReadable = true;
96
97 // we have implemented the _read method, and done the other things
98 // that Readable wants before the first _read call, so unset the
99 // sync guard flag.
100 this._readableState.sync = false;
101
102 if (options) {
103 if (typeof options.transform === 'function') this._transform = options.transform;
104
105 if (typeof options.flush === 'function') this._flush = options.flush;
106 }
107
108 this.once('prefinish', function () {
109 if (typeof this._flush === 'function') this._flush(function (er) {
110 done(stream, er);
111 });else done(stream);
112 });
113}
114
115Transform.prototype.push = function (chunk, encoding) {
116 this._transformState.needTransform = false;
117 return Duplex.prototype.push.call(this, chunk, encoding);
118};
119
120// This is the part where you do stuff!
121// override this function in implementation classes.
122// 'chunk' is an input chunk.
123//
124// Call `push(newChunk)` to pass along transformed output
125// to the readable side. You may call 'push' zero or more times.
126//
127// Call `cb(err)` when you are done with this chunk. If you pass
128// an error, then that'll put the hurt on the whole operation. If you
129// never call cb(), then you'll never get another chunk.
130Transform.prototype._transform = function (chunk, encoding, cb) {
131 throw new Error('Not implemented');
132};
133
134Transform.prototype._write = function (chunk, encoding, cb) {
135 var ts = this._transformState;
136 ts.writecb = cb;
137 ts.writechunk = chunk;
138 ts.writeencoding = encoding;
139 if (!ts.transforming) {
140 var rs = this._readableState;
141 if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark);
142 }
143};
144
145// Doesn't matter what the args are here.
146// _transform does all the work.
147// That we got here means that the readable side wants more data.
148Transform.prototype._read = function (n) {
149 var ts = this._transformState;
150
151 if (ts.writechunk !== null && ts.writecb && !ts.transforming) {
152 ts.transforming = true;
153 this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform);
154 } else {
155 // mark that we need a transform, so that any data that comes in
156 // will get processed, now that we've asked for it.
157 ts.needTransform = true;
158 }
159};
160
161function done(stream, er) {
162 if (er) return stream.emit('error', er);
163
164 // if there's nothing in the write buffer, then that means
165 // that nothing more will ever be provided
166 var ws = stream._writableState;
167 var ts = stream._transformState;
168
169 if (ws.length) throw new Error('Calling transform done when ws.length != 0');
170
171 if (ts.transforming) throw new Error('Calling transform done when still transforming');
172
173 return stream.push(null);
174}