07
Streams: an array rotated onto the time axis, with pressure
A stream must deliver every value, in order, to one consumer. Producer and consumer rarely run at the same rate, so a buffer sits between them: two promise queues, one carrying values forward and one carrying acknowledgements back. Each
in.yield() returns a promise for an ack; a producer that waits for it can never outrun the consumer by more than the buffer's length.On the producer side, a vacuum stalls the consumer and a pressure sends values forward. On the consumer side, a vacuum draws values forward and pressure, often called back pressure, stalls the producer.
Producer → buffer → consumer
producer · in.yield(n)
0
idle
buffer · outbound queue2 unacked writes allowed
values →← acks
consumer · forEach(job, null, 1)
0
#0starving
throughput
0.0/s
max possible
3/s
producer stalled
—
bottleneck
consumer
a promise buffer: two queues, entangled
var outbound = new PromiseQueue();
var inbound = new PromiseQueue();
var buffer = {
out: { // the reader: an async iterator
next: function (value) {
outbound.put({ value: value, done: false });
return inbound.get();
},
// return, throw ...
},
in: { // the writer: an async generator
yield: function (value) {
inbound.put({ value: value, done: false });
return outbound.get(); // a promise for the consumer's ack
},
// return, throw ...
}
};pressure: the producer idles until acked
var buffer = new Buffer(1024); // prime 1024 acks
function fibStream(a, b) {
return buffer.in.yield(a)
.then(function () { // wait: this is the pressure
return fibStream(b, a + b);
});
}
fibStream(1, 1).done();
return buffer.out;
// later, from the consumer side:
buffer.out.throw(new Error("That's enough, thanks"));Iterator vocabulary → stream vocabulary
| in.yield(value) | means “write” |
| in.return() | means “close” |
| in.throw(error) | means “terminate prematurely with an error” |
| out.next() | means “read” |
| out.throw(error) | means “abort or cancel with an error” |
| out.return() | means “abort or cancel prematurely, without an error” |
Fork vs share: one stream, a slow map and a fast map
Both maps call next() on the same reader: each value goes to whichever consumer asks first.
slow map
delay(200)
0 seen
0.0/s
fast map
delay(100)
0 seen
0.0/s
source produced
0
total throughput
0.0/s
fast : slow
—
expected
≤ 15/s, 2 : 1
two consumers of one stream
var slow = stream.map(function (n) {
return Promise.return(n).delay(200);
});
var fast = stream.map(function (n) {
return Promise.return(n).delay(100);
});Streams are unicast: the consumer expects every value, so information flows both ways and either side can end the flow. That's also what makes them cancelable.
Handing one reader to two consumers gives round-robin load balancing: each sees an exclusive part of the stream, and pressure can only be lower than with one consumer — up to 15 values per second here. A fork sends every value to each branch; the slowest branch sets the pressure, so it can only be higher. Lower the source rate below 10/s and the shared reader distributes values fairly.
- why it mattersBack pressure is just a promise the producer waits on. Make the producer faster than the consumer and watch it spend most of its time stalled, while the buffer stays bounded.
- why it matters
Buffer(length)primes the ack queue: it trades memory for latency, letting the producer run ahead so the consumer rarely starves. Concurrency inforEachraises the consumer's rate instead. - why it mattersBecause the consumer talks back, it can stop the producer:
out.throw()rejects the producer's pendingyield. A promise could never do that — it's broadcast.