rafa
Version:
Rafa.js is a Javascript framework for building concurrent applications.
530 lines (478 loc) • 17 kB
JavaScript
// # constructor(Stream, handler: Message => Message): Stream
// A Stream is the protagonist.
function Stream(parent, handler) {
this.channel = null;
this.child = null;
this.destroy = null;
this.handler = handler || this.identity;
this.next = null;
this.parent = parent || null;
this.prev = null;
if (parent) {
this.next = parent.child;
if (parent.child) parent.child.prev = this;
parent.child = this;
}
}
inherit(Stream, Rafa, {
// # all(handler: Message => Message): Stream
// Return a new child Stream that handles all Messages
all(handler) {
return new Stream(this, handler);
},
// # collect(collector: A => B)
// Return a new child stream whose output values are the result of applying
// the `collector` function to each input value. If the result value of the
// `collector` is `null` or `undefined` then child streams will not be
// notified.
//
// Think of this option as a combination of `map` and `filter`.
collect(collector) {
return this.all(message => message.collect(collector));
},
// # detach()
// Detach this stream from its parent.
detach() {
const prev = this.prev;
const next = this.next;
const parent = this.parent;
const destroy = this.destroy;
if (prev) prev.next = next;
else if (parent) parent.child = next;
if (next) next.prev = prev;
if (destroy) destroy();
},
// # done(unit: A => _): Stream
// Return a new child Stream that handles the DoneMessage, which also may
// contain the last value.
done(unit) {
return this.all(message => {
if (message.isDone) unit(message.value);
return message;
});
},
// # drop(n): Stream
// Return a new child stream that ignores the first `n` messages and pushes
// all subsequent messages.
drop(n) {
let i = Math.max(n, 0);
return this.all(message => {
if (i > 0 && message.isValue) {
i--;
return;
}
else if (message.isDone) i = n;
return message;
});
},
// # each(unit: A => _): Stream
// Return a new child Stream that handles all Messages with a value
// (Message & DoneMessage)
each(unit) {
return this.all(message => {
if (message.isValue) unit(message.value);
return message;
});
},
// # enumerate(Enumerator): Stream
// Send values provided by an Enumerator through the stream.
enumerate(enumerator) {
const next = () => enumerator.next(callback);
const context = message => this.context(!message.isDone && next);
const callback = message => this.push(context(message), message);
next();
return this;
},
// # error(unit: Error => _): Stream
// Return a new child Stream that handles Error Messages
error(unit) {
return this.all(message => {
if (message.isError) unit(message.error);
return message;
});
},
// # filter(predicate: A => Bool): Stream
// Return a new child stream whose output values are filtered by applying
// the predicate funciton `ab` to each input value where `ab` is a function
// that accepts a single value and returns a boolean.
filter(predicate) {
return this.all(message => message.filter(predicate));
},
// # filterDuplicates(): Stream
// Return a new child stream whose values are never the same for two
// consecutive messages.
filterDuplicates() {
let lastValue;
return this.filter(value => {
const ok = value !== lastValue;
if (ok) lastValue = value;
return ok;
});
},
// # flatMap(mapper: A => Stream): Stream
// Return a new child stream that receives multiple messages for each
// parent message. The mapper function returns a new stream.
flatMap(mapper) {
return this.all(function(message, context) {
if (message.isValue) {
const child = this.child;
context.wait();
mapper(message.value).all((m, c) => {
if (child) {
context.wait(); c.wait();
child.push(
this.context(() => { c.end(); context.end(); }),
m.isDone && !message.isDone ? m.toValue() : m
);
}
if (m.isDone) context.end();
});
}
else return message;
});
},
// # fold(seed: A, folder: (A,A) => A): Stream
// Fold over stream values by applying the folder function to values and an
// initial value.
fold(seed, folder) {
let value = seed;
return this.all(message => {
let m = message;
if (m.isValue) {
m = m.fold(value, folder);
value = m.value;
if (m.isDone) return m;
}
});
},
// # group(...categories: String[, handler: A => String]): Stream
// Return a new Object whose keys are Category Strings
// and values are child Streams. The result can be visualized as a fan-out
// structure. Callers provide Category Strings as parameters and an optional
// handler that, given a Message Value, returns a Category String. All
// Messages that don"t match a Category are sent through to the default
// Category named with an underscore `_`.
group: (function() {
return function(...categories/*, handler*/) {
let category, stream;
let i = categories.length;
let handler = categories[i - 1];
if (typeof handler === "function") i--;
else handler = this.identity;
const parts = { _: this.stream() };
while (i--) {
stream = this.stream();
category = categories[i];
parts[category] = stream;
stream.destroy = destroy(parts, category);
}
parts._.destroy = destroy(parts, "_");
this.all(group(parts, handler));
return parts;
};
// Closure wrapper that prevents variables in the group function from
// being kept in memory (except parts & handler). The returned function
// is a function that will be called for each Message and push the
// Message to the correct Category.
function group(parts, handler) {
return (message, context) => {
const category = String(handler(message.value));
const stream = (parts[category] || parts._);
if (stream) stream.push(context, message);
};
}
// Closure wrapper that prevents variables in the group function from
// being kept in memory (except parts & category). The returned function is
// called when a child Stream is detached. It will set the Partition
// to `null` so new Messages will no longer flow through to the child
// Partition. However, they will continue to flow to the default
// Partition (`_`).
function destroy(parts, category) {
return () => parts[category] = null;
}
})(),
// # listener(kind: String, capacity: Int, action: String): ((...args) => _)
// Return a function that handles a variable number of arguments, converts
// those arguments to a single value, and then writes that value to a
// channel. The returned function is meant to be used as an event handler.
listener(kind = "", capacity = 0, action) {
const M = this[kind + "Message"] || this.Message;
let channel = this.channel;
if (channel) {
channel.configure(
Math.max(capacity, channel.buffer ? channel.buffer.capacity : 0),
action
);
}
else {
channel = new this.Channel(capacity, action);
this.channel = channel;
this.enumerate(channel);
}
return this.params(params =>
channel.write(new M(params.length > 1 ? params : params[0]))
);
},
// # map(ab: A => B): Stream
// Return a new child stream whose output values are the result of applying
// the function `ab` to each input value where `ab` is a function of
// `a => b`.
map(ab) {
return this.all(message => message.map(ab));
},
// # merge(...Stream): Stream
// Return a new Stream that will produce all Messages from
// the current Stream and all Streams provided as parameters. The
// result can be visualized as a fan-in structure. The new Stream
// will produce values and errors from all Streams and a single done event
// after all Streams complete. The method will create proxy Streams that
// push messages from a single parent Stream to the child Stream.
merge: (function() {
return function(...mergeStreams) {
const streams = [this].concat(mergeStreams);
const child = this.stream();
const state = { active: streams.length };
const handler = merge(child, state);
const len = streams.length;
const proxies = [];
let i = 0;
for (; i < len; i++) {
proxies[i] = new this.Stream(streams[i], handler);
}
child.destroy = destroy(proxies);
state.streams = state.active;
return child;
};
// Closure wrapper that prevents variables in the merge function from
// being kept in memory (except child & state). The returned function
// is a function that will be called by all proxy Streams. It proxies
// all Messages from all parent Streams to the child Stream and makes
// sure to only send a single Done Message.
function merge(child, state) {
return function(message, context) {
if (message.isDone) {
if (--state.active) child.push(context, this.message(message.value));
else {
state.active = state.streams;
child.push(context, message);
}
}
else child.push(context, message);
return message;
};
}
// Closure wrapper that prevents variables in the merge function from
// being kept in memory (except proxies). The returned function is
// called when the child Stream is detached. It will call `detach` on
// all proxy Streams.
function destroy(proxies) {
return function() {
let i = proxies.length;
while (i--) proxies[i].detach();
};
}
})(),
// # once(): Stream
// Return a new child stream that emits a single done message when the
// first upstream message is received. The upstream message is converted
// to a done message.
once() {
return this.all(function(message) {
this.detach();
return message.toDone();
});
},
// # push(Context, Message)
// Breadth first traversal of the stream tree.
push(context, message) {
context.wait();
if (message.value && message.value.then) {
context.waitfor(this, message);
}
else {
if (this.next) this.next.push(context, message);
var m = this.handler(message, context);
if (m && this.child) this.child.push(context, m);
}
context.end();
},
// # recover(map: Error => A): Stream
// Return a new child Stream that handles ErrorMessage and maps the error
// to a Message. Normal Message and DoneMessage messages pass through.
recover(map) {
return this.all(message => {
let m = message;
if (m.isError) m = this.message(map(m.error));
return m;
});
},
// # reduce(folder: (A,A) => A): Stream
// Reduce the stream by applying the folder function to stream values.
reduce(folder) {
let value;
return this.all(message => {
let m = message;
if (!m.isError) {
if (value !== undefined) {
m = m.fold(value, folder);
value = m.value;
}
else value = m.value;
if (m.isDone) return m;
}
});
},
// # release(): Stream
// Release backpressure. Downstream nodes that perform a flatMap or return
// a Promise will not prevent upstream nodes from sending messages.
release() {
return this.all(function(message) {
this.push(this.context(), message);
});
},
// # scan(scanner: (A,A) => A): Stream
// Return a new child stream whose values are the result of applying a
// scanner function to the current message value and the previous scanner
// function result. This is like fold except it emits a message for every
// upstream message.
scan(seed, scanner) {
let value = seed;
return this.all(message => {
let m = message;
if (m.isValue) {
m = m.fold(value, scanner);
value = m.value;
}
return m;
});
},
// # split(splitter: A => [A]): Stream
// Return a new child stream whose values are the result of applying a
// splitter function to each incoming message. If the splitter function
// returns an array of values, then each of those values are sent
// individually to the child stream, otherwise the original value is
// returned.
split(splitter) {
return this.all(function(message, context) {
const child = this.child;
if (child) {
if (!message.isValue) return message;
var items = splitter(message.value);
var last = items.length - 1;
for (var i = 0; i < last; i++)
child.push(context, this.message(items[i]));
child.push(context, message.copy(items[i]));
}
});
},
// # take(n): Stream
// Return a new child stream that pushes the first `n` messages to child
// streams and resets when it receives a done message.
take(n) {
let i = Math.max(n, 0);
return this.all(function(message) {
if (message.isValue) {
if (i > 0) i--;
else return;
}
else if (message.isDone) i = n;
return message;
});
},
// # toProperty(seed: A): Property
// Return a new Property object whose value initially contains `seed` and
// is updated/set when a new value is received from upstream nodes.
toProperty(seed) {
const property = this.property(seed);
this.all((message, context) => property.set(message.value, context));
return property;
},
// # until(Stream): Stream
// Return a new child stream that emits values until the stream parameter
// emits a value. Once the parameter stream emits, that value is sent as
// a done message and all streams are detached.
until(stream) {
const outer = this.all(function(message) {
if (message.isDone) {
inner.detach();
this.detach();
}
return message;
});
const inner = stream.all((message, context) => {
outer.push(context, message.toDone());
});
return outer;
},
// # write(value: A | Error, isDone: Bool): Stream
// Return this stream node after pushing a value or error through the stream.
// If value is an instance of Error, then an error message is pushed, otherwise
// a value message is pushed. Note that using this funciton will not cause
// the stream to wait for new messages. This is simply a simple way to
// send one-off messages. To utilize backpressure, use a Channel or
// Enumerator.
write(value, isDone) {
let message;
if (value instanceof Error) {
if (isDone) message = this.errordoneMessage(value);
else message = this.errorMessage(value);
}
else if (isDone) message = this.doneMessage(value);
else message = this.message(value);
this.push(this.context(), message);
return this;
},
// # zip(...Stream): Stream
// Return a new child Stream whose value is an array containing the parent
// stream's next value along with values from all streams provided as
// arguments. A value is not pushed until all streams produce a value.
// Errors pass through and a done message is not sent until all streams
// are done. Once a stream is done, future messages will contain undefined
// in the array.
zip: (function() {
return function(...streams) {
const len = streams.length + 1;
const proxies = new Array(len);
const state = { active: len, pending: len, cache: new Array(len) };
const child = this.stream();
proxies[0] = this.all(bind(state, child, 0, len));
for (let i = 1; i < len; i++)
proxies[i] = streams[i - 1].all(bind(state, child, i, len));
child.destroy = destroy(proxies);
return child;
};
function bind(state, child, index, len) {
return (message, context) =>
handle(message, context, state, child, index, len);
}
function handle(message, context, state, child, index, len) {
let M = message.Message;
if (message.isDone) state.active--;
if (!state.cache[index]) state.pending--;
if (message.isError) {
child.push(context, new message.ErrorMessage(message.error));
}
else state.cache[index] = message;
if (!state.active) {
M = message.DoneMessage;
state.active = len;
}
if (!state.pending) {
const values = new Array(len);
for (let i = 0, value; i < len; i++) {
value = state.cache[i];
if (value) values[i] = value.value;
state.cache[i] = null;
}
state.pending = len;
child.push(context, new M(values));
}
}
function destroy(proxies) {
return () => {
let i = proxies.length;
while (i--) proxies[i].detach();
};
}
})()
});