UNPKG

rafa

Version:

Rafa.js is a Javascript framework for building concurrent applications.

530 lines (478 loc) 17 kB
// # 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(); }; } })() });