Created
August 5, 2016 14:56
-
-
Save webstrand/d66722bc4f604b79a75de3cac4c6a548 to your computer and use it in GitHub Desktop.
Prototype pub-sub library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Module streams | |
| // | |
| // This is effectively a pub-sub system with a few extra convenience methods. | |
| // Fundamentally, observers are attached to a stream or property, and are called | |
| // when the stream or property emits an event. | |
| // | |
| // By and large, most of the effort in this module is devoted to creating | |
| // optimized streams and properties for passing around an arbitrary amount | |
| // argument variables rather than wrapping up multi-part events into arrays. | |
| // | |
| // In some cases, this method is faster because the Javascript VM doesn't have | |
| // to pack and unpack arrays as each observer reads the event. And none of the | |
| // observers can accidentally mutate the event object itself. | |
| // | |
| // To generate the various argument-width streams and properties, functions are | |
| // dynamically generated at runtime. There is a slight performance hit due to | |
| // this compilation, but for long-running apps the effect is negligible. | |
| // | |
| // | |
| // There are two fundamental types that are generated by this module: | |
| // | |
| // First are streams, which are simply publisher objects. The various argument- | |
| // widths from 0 to 6 (currently) can be accessed through the Stream0 .. Stream6 | |
| // constructors. | |
| // | |
| // Second are properties, which are like streams, except that they persist the | |
| // last event they emitted and immediately re-emit that saved event to any new | |
| // observer that is bound. The various argument-widths from 0 to 6 (currently) | |
| // can be accessed through the Property0 .. Property6 constructors. | |
| (function (exports) { | |
| "use strict"; | |
| // Argument names used in dynamically generated functions. | |
| var arg_names = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', | |
| 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', | |
| 'w', 'x', 'y', 'z' ]; | |
| // Many of the higher-order functions require an index mapping integers to | |
| // Stream and Property widths. | |
| var subordinate_stream_index = []; | |
| var subordinate_property_index = []; | |
| // Prototype shared by all Stream objects regardless of width. | |
| var shared_stream_prototype = { | |
| stream__stream_index: subordinate_stream_index, | |
| stream__property_index: subordinate_property_index, | |
| /** | |
| * stream.on_event(fn, context) | |
| * | |
| * Attach a new observer to the stream, and return a handle for later | |
| * removal. Observers will be called for each new event in the order | |
| * that they were attached. | |
| * | |
| * @param {Function} fn Event callback function. Should consume as many | |
| * arguments as the Stream's width. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @returns {Handle} Handle of callback binding. | |
| */ | |
| on_event: function (fn, context) { | |
| this.stream__observers.push(fn, context); | |
| return [fn, context]; | |
| }, | |
| /** | |
| * stream.off_event(handle) | |
| * | |
| * Remove an observer from the stream. | |
| * | |
| * @param {Handle} handle Handle returned by on_event. | |
| */ | |
| /** | |
| * stream.off_event(fn, context) | |
| * | |
| * Remove an observer from the stream. Only one instance of fn and | |
| * context are removed from the stream. | |
| * | |
| * @param {Function} fn Event callback function. | |
| * @param {Object} context Context in which to call fn. | |
| */ | |
| off_event: function (fn_or_handle, ctxt) { | |
| // Decide if we're being called with a handle or a function-context | |
| // pair. | |
| var fn, context; | |
| if(fn_or_handle instanceof Array) { | |
| fn = fn_or_handle[0]; | |
| context = fn_or_handle[1]; | |
| } | |
| else { | |
| fn = fn_or_handle; | |
| context = ctxt; | |
| } | |
| // Iterate through the observers until something matches our | |
| // function-context pair, then splice it out. | |
| var observers = this.stream__observers; | |
| for(var i = 0, length = observers.length; i !== length; i += 2) { | |
| if(observers[i] === fn && observers[i + 1] === context) { | |
| observers.splice(i, 2); | |
| break; | |
| } | |
| } | |
| }, | |
| /** | |
| * stream.off_event_all() | |
| * | |
| * Remove all observers from the stream. | |
| */ | |
| off_event_all: function () { | |
| this.stream__observers = []; | |
| }, | |
| /** | |
| * stream.to_stream() | |
| * | |
| * Convert the property or stream into a stream. When called on a | |
| * property, a new object may be created. When called on streams the | |
| * stream is returned. | |
| * | |
| * @returns {this} | |
| */ | |
| to_stream: function () { | |
| return this; | |
| }, | |
| /** | |
| * stream.destroy() | |
| * | |
| * Destroy the stream, removing all observers and any associated | |
| * metadata. | |
| */ | |
| destroy: null | |
| }; | |
| // For now, destroy is just an alias. | |
| shared_stream_prototype.destroy = shared_stream_prototype.off_event_all; | |
| var shared_subordinate_stream_prototype = { | |
| // See stream.on_event | |
| on_event: function (fn, context) { | |
| // If the stream isn't active, activate the stream. | |
| if(this.stream__active === false) { | |
| this.stream__active = true; | |
| this.stream__open(); | |
| } | |
| this.stream__observers.push(fn, context); | |
| return [fn, context]; | |
| }, | |
| // See stream.off_event | |
| off_event: function(fn_or_handle, ctxt) { | |
| // Decide if we're being called with a handle or a function-context | |
| // pair. | |
| var fn, context; | |
| if(fn_or_handle instanceof Array) { | |
| fn = fn_or_handle[0]; | |
| context = fn_or_handle[1]; | |
| } | |
| else { | |
| fn = fn_or_handle; | |
| context = ctxt; | |
| } | |
| // Iterate through the observers until something matches our | |
| // function-context pair, then splice it out. | |
| var observers = this.stream__observers; | |
| for(var i = 0, length = observers.length; i !== length; i += 2) { | |
| if(observers[i] === fn && observers[i + 1] === context) { | |
| observers.splice(i, 2); | |
| // Close the stream, because if there was a single observer, | |
| // then the stream was open. Now that one observer has been | |
| // removed, there are no observers. | |
| if(length === 2) { | |
| this.stream__active = false; | |
| this.stream__close(); | |
| } | |
| break; | |
| } | |
| } | |
| }, | |
| // See stream.off_event_all | |
| off_event_all: function() { | |
| this.stream__observers = []; | |
| this.stream__active = false; | |
| this.stream__close(); | |
| }, | |
| // See stream.destroy | |
| destroy: null | |
| }; | |
| // For now, destroy is just an alias. | |
| shared_subordinate_stream_prototype.destroy = shared_subordinate_stream_prototype.off_event_all; | |
| /** | |
| * produce_streamN_prototype(width) | |
| * | |
| * Produce a prototype for a Stream of the given width. | |
| * | |
| * @returns {Object} | |
| */ | |
| function produce_streamN_prototype(width) { | |
| var args = arg_names.slice(0, width); | |
| // Curried version of Function with all of the arguments bound to the | |
| // args identifiers. | |
| var function_args = Function.bind.apply(Function, [null].concat(args)); | |
| // The args identifiers in a comma list | |
| var args_list = args.join(', '); | |
| // The args identifiers with a comma preceding the first identifier. | |
| // Empty string if width = 0. | |
| var args_list_f = args.reduce(function (s, a) { return s + ', ' + a }, ''); | |
| var prototype = { | |
| /** | |
| * stream.emit(a, b, ...) | |
| * | |
| * Immediately emit a new event to all currently existing observers. | |
| * Observers attached during emission will be ignored for this | |
| * event, but will receive later events. | |
| * | |
| * The stream may be mutated during emission. Any alterations, such | |
| * as adding or removing observers will not effect any ongoing | |
| * emission, only successive events. | |
| */ | |
| emit: new function_args('\ | |
| "use strict";\ | |
| var observers = this.stream__observers.slice();\ | |
| for(var i = 0, length = observers.length; i !== length; i += 2) {\ | |
| var observer = observers[i];\ | |
| var context = observers[i + 1];\ | |
| observer.call(context' + args_list_f + ');\ | |
| }\ | |
| ' ), | |
| /** | |
| * stream.map1(fn, context) | |
| * | |
| * Create a new subordinate stream of width 1 and map events from | |
| * the superordinate stream to the subordinate stream by way of fn. | |
| * | |
| * @param {Function} fn Event mapping function. Should consume as | |
| * many arguments as the superordinate stream's width. It's return | |
| * value is used as the event emitted by the subordinate stream. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @returns {SubordinateStream} | |
| */ | |
| map1: new Function('fn', 'context', '\ | |
| "use strict";\ | |
| var superordinate_stream = this;\ | |
| function map__observer(' + args_list + ') { this.emit(fn.call(context' + args_list_f + ')); };\ | |
| return new this.stream__stream_index[1](function map__open() {\ | |
| superordinate_stream.on_event(map__observer, this);\ | |
| }, function map__close() {\ | |
| superordinate_stream.off_event(map__observer, this);\ | |
| });\ | |
| ' ), | |
| /** | |
| * stream.map(fn, context, width) | |
| * | |
| * Create a new subordinate stream of the given width and map events | |
| * from the superordinate stream to the subordinate stream by way of | |
| * fn. | |
| * | |
| * @param {Function} fn(a, b, ...) Event mapping function. Should | |
| * consume as many arguments as the superordinate stream's width. | |
| * It's return must be an array with a length of the subordinate | |
| * stream's width. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @param {Integer} width Width of the subordinate stream. If left | |
| * undefined, the width is inherited. | |
| * | |
| * @returns {SubordinateStream} | |
| */ | |
| map: new Function('fn', 'context', 'width', '\ | |
| "use strict";\ | |
| var superordinate_stream = this;\ | |
| function map__observer(' + args_list + ') {\ | |
| this.emit.apply(this, fn.call(context' + args_list_f + '));\ | |
| };\ | |
| return new this.stream__stream_index[(width === undefined ? ' + width + ' : width)](function map__open() {\ | |
| superordinate_stream.on_event(map__observer, this);\ | |
| }, function map__close() {\ | |
| superordinate_stream.off_event(map__observer, this);\ | |
| });\ | |
| ' ), | |
| /** | |
| * stream.mapfn(fn, context, width) | |
| * | |
| * Create a new subordinate stream of the given width and map events | |
| * from the superordinate stream to the subordinate stream by way of | |
| * fn. | |
| * | |
| * Experimental variation of map1 and map which uses a callback to | |
| * emit events on the subordinate stream rather than the return | |
| * value. | |
| * | |
| * @param {Function} fn(emit, a, b, ...) [description] | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @param {Integer} width Width of the subordinate stream. If left | |
| * undefined, the width is inherited. | |
| * | |
| * @returns {SubordinateStream} | |
| */ | |
| mapfn: new Function('fn', 'context', 'width', '\ | |
| "use strict";\ | |
| var superordinate_stream = this;\ | |
| function mapfn__observer(' + args_list + ') {\ | |
| fn.call(context, this.stream__mapfn_emit' + args_list_f + ')\ | |
| }\ | |
| var stream = new this.stream__stream_index[(width === undefined ? ' + width + ' : width)](function mapfn__open() {\ | |
| superordinate_stream.on_event(mapfn__observer, this);\ | |
| }, function mapfn__close() {\ | |
| superordinate_stream.off_event(mapfn__observer, this);\ | |
| });\ | |
| stream.stream__mapfn_emit = function (' + args_list + ') {\ | |
| stream.emit(' + args_list + ');\ | |
| };\ | |
| return stream;\ | |
| ' ), | |
| /** | |
| * stream.filter(fn, context) | |
| * | |
| * Create a new subordinate stream and re-emit events that are | |
| * accepted by the filter function. | |
| * | |
| * @param {Function} fn Event filter function. Should consume as | |
| * many arguments as the stream's width. It's return value will be | |
| * cast to boolean, and if true the event will be re-emitted. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @returns {SubordinateStream} | |
| */ | |
| filter: new Function('fn', 'context', '\ | |
| "use strict";\ | |
| var superordinate_stream = this;\ | |
| function filter__observer(' + args_list + ') { if(fn.call(context' + args_list_f + ') === true) this.emit(' + args_list + '); };\ | |
| return new this.stream__stream_index[' + width + '](function filter__open() {\ | |
| superordinate_stream.on_event(filter__observer, this);\ | |
| }, function filter__close() {\ | |
| superordinate_stream.off_event(filter__observer, this);\ | |
| });\ | |
| ' ), | |
| /** | |
| * stream.to_property(a, b, ...) | |
| * | |
| * Create a new subordinate property, which consumes and re-emits | |
| * events. The initial values of the property is defined in the | |
| * arguments passed to this function. | |
| * | |
| * @returns {SubordinateProperty} | |
| */ | |
| to_property: new function_args('\ | |
| "use strict";\ | |
| var superordinate_stream = this;\ | |
| return new this.stream__property_index[' + width + '](function to_property__open() {\ | |
| superordinate_stream.on_event(this.emit, this);\ | |
| }, function to_property__close() {\ | |
| superordinate_stream.off_event(this.emit, this);\ | |
| }' + args_list_f + ');\ | |
| ' ), | |
| }; | |
| return extend(prototype, shared_stream_prototype); | |
| } | |
| /** | |
| * produce_subordinate_streamN_prototype(streamN_prototype, width) | |
| * | |
| * Produce a prototype for a SubordinateStream of the given width. | |
| * | |
| * Because this prototype requires many of the functions provided by a | |
| * regular Stream's prototype, streamN_prototype must be provided. | |
| * | |
| * @returns {Object} | |
| */ | |
| function produce_subordinate_streamN_prototype(streamN_prototype, width) { | |
| return extend({}, streamN_prototype, shared_subordinate_stream_prototype); | |
| } | |
| // Prototype shared by all Property objects regardless of width. | |
| var shared_property_prototype = { | |
| property__stream_index: subordinate_stream_index, | |
| property__property_index: subordinate_property_index, | |
| /** | |
| * property.off_event(fn, context) | |
| * | |
| * Remove an observer from the property. Only one instance of fn and | |
| * context are removed from the property. | |
| * | |
| * @param {Function} fn Event callback function. | |
| * @param {Object} context Context in which to call fn. | |
| */ | |
| off_event: function(fn_or_handle, ctxt) { | |
| var fn, context; | |
| if(fn_or_handle instanceof Array) { | |
| fn = fn_or_handle[0]; | |
| context = fn_or_handle[1]; | |
| } | |
| else { | |
| fn = fn_or_handle; | |
| context = ctxt; | |
| } | |
| var observers = this.property__observers; | |
| for(var i = 0, length = observers.length; i !== length; i += 2) { | |
| if(observers[i] === fn && observers[i + 1] === context) { | |
| observers.splice(i, 2); | |
| break; | |
| } | |
| } | |
| }, | |
| /** | |
| * property.off_event_all() | |
| * | |
| * Remove all observers from the property. | |
| */ | |
| off_event_all: function() { | |
| this.property__observers = []; | |
| }, | |
| /** | |
| * property.to_property() | |
| * | |
| * Convert the property or stream into a property. When called on a | |
| * stream, a new object may be created. When called on properties the | |
| * property is returned. | |
| * | |
| * @returns {this} | |
| */ | |
| to_property: function () { | |
| return this; | |
| }, | |
| /** | |
| * property.destroy() | |
| * | |
| * Destroy the property, removing all observers and any associated | |
| * metadata. | |
| */ | |
| destroy: null | |
| }; | |
| // For now, destroy is just an alias. | |
| shared_property_prototype.destroy = shared_property_prototype.off_event_all; | |
| /** | |
| * produce_propertyN_prototype(width) | |
| * | |
| * Produce a prototype for a property of the given width. | |
| * | |
| * @returns {Object} | |
| */ | |
| function produce_propertyN_prototype(width) { | |
| var args = arg_names.slice(0, width); | |
| // Curried version of Function with all of the arguments bound to the | |
| // args identifiers. | |
| var function_args = Function.bind.apply(Function, [null].concat(args)); | |
| // Curried version of Function with the arguments bound to a 'fn' and | |
| // 'context' followed by all of the args identifiers. | |
| var function_fn_args = Function.bind.apply(Function, [null, 'fn', 'context'].concat(args)); | |
| // The args identifiers in a comma list | |
| var args_list = args.join(', '); | |
| // The args identifiers with a comma preceding the first identifier. | |
| // Empty string if width = 0. | |
| var args_list_f = args.reduce(function (s, a) { return s + ', ' + a }, ''); | |
| // The args identifiers prefixed with prev_ with a comma preceding the | |
| // first identifier. Empty string if width = 0. | |
| var args_list_prev_f = args.reduce(function (s, a) { return s + ', prev_' + a }, ''); | |
| // A list of nulls with a comma preceding the first null. Empty string | |
| // if width = 0. | |
| var null_list_f = args.reduce(function (s, a) { return s + ', null' }, ''); | |
| // The args identifiers prefixed with 'this.property__' with a comma | |
| // preceding the first identifier. Empty string if width = 0. | |
| var current_values_f = args.reduce(function (s, a) { return s + ', this.property__' + a }, ''); | |
| // Code fragment which saves previous values from 'this.property__' to | |
| // 'prev_'. | |
| var save_values = args.reduce(function (s, a) { return s + 'var prev_' + a + ' = this.property__' + a + ';' }, ''); | |
| // Code fragment which saves the args identifiers to 'this.property__'. | |
| var write_values = args.reduce(function (s, a) { return s + 'this.property__' + a + ' = ' + a + ';' }, ''); | |
| var prototype = { | |
| /** | |
| * property.on_event(fn, context) | |
| * | |
| * Attach a new observer to the property, and return a handle for | |
| * later removal. Observers will be called immediately after | |
| * attachment with the current values. Then they will be called for | |
| * each new event in the order that they were attached. | |
| * | |
| * @param {Function} fn Event callback function. Should consume | |
| * twice as many arguments as the Property's width. The first set | |
| * are the current values, the second set are the previous values. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @returns {Handle} Handle of callback binding. | |
| */ | |
| on_event: new Function('fn', 'context', '\ | |
| "use strict";\ | |
| this.property__observers.push(fn, context);\ | |
| fn.call(context' + current_values_f + null_list_f + ');\ | |
| return [fn, context];\ | |
| ' ), | |
| /** | |
| * property.emit(a, b, ...) | |
| * | |
| * Update the current values, then immediately emit a new event to | |
| * all currently existing observers. Observers attached during | |
| * emission will be ignored for this event, but will receive later | |
| * events. | |
| * | |
| * The property may be mutated during emission. Any alterations, such | |
| * as adding or removing observers will not effect any ongoing | |
| * emission, only successive events. | |
| */ | |
| emit: new function_args('\ | |
| "use strict";\ | |
| ' + save_values + '\ | |
| ' + write_values + '\ | |
| var observers = this.property__observers.slice();\ | |
| for(var i = 0, length = observers.length; i !== length; i += 2) {\ | |
| var observer = observers[i];\ | |
| var context = observers[i + 1];\ | |
| observer.call(context' + args_list_f + args_list_prev_f + ');\ | |
| }\ | |
| ' ), | |
| /** | |
| * property.map1(fn, context) | |
| * | |
| * Create a new subordinate property of width 1 and map events from | |
| * the superordinate property to the subordinate property by way of | |
| * fn. | |
| * | |
| * @param {Function} fn Event mapping function. Should consume | |
| * twice as many arguments as the Property's width. The first set | |
| * are the current values, the second set are the previous values. | |
| * Should consume as It's return value is used as the event emitted | |
| * by the subordinate property. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @returns {SubordinateProperty} | |
| */ | |
| map1: new Function('fn', 'context', '\ | |
| "use strict";\ | |
| var superordinate_property = this;\ | |
| function map__observer(' + args_list + args_list_prev_f + ') {\ | |
| this.emit(fn.call(context' + args_list_f + args_list_prev_f + '));\ | |
| };\ | |
| return new this.property__property_index[1](function map__open() {\ | |
| superordinate_property.on_event(map__observer, this);\ | |
| }, function map__close() {\ | |
| superordinate_property.off_event(map__observer, this);\ | |
| });\ | |
| ' ), | |
| /** | |
| * property.map(fn, context, width) | |
| * | |
| * Create a new subordinate property of the given width and map | |
| * events from the superordinate property to the subordinate | |
| * property by way of fn. | |
| * | |
| * @param {Function} fn Event mapping function. Should consume | |
| * twice as many arguments as the Property's width. The first set | |
| * are the current values, the second set are the previous values. | |
| * It's return must be an array with a length of the subordinate | |
| * property's width. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @param {Integer} width Width of the subordinate stream. If left | |
| * undefined, the width is inherited. | |
| * | |
| * @returns {SubordinateProperty} | |
| */ | |
| map: new Function('fn', 'context', 'width', '\ | |
| "use strict";\ | |
| var superordinate_property = this;\ | |
| function map__observer(' + args_list + args_list_prev_f + ') {\ | |
| this.emit.apply(this, fn.call(context' + args_list_f + args_list_prev_f + '));\ | |
| };\ | |
| return new this.property__property_index[(width === undefined ? ' + width + ' : width)](function map__open() {\ | |
| superordinate_property.on_event(map__observer, this);\ | |
| }, function map__close() {\ | |
| superordinate_property.off_event(map__observer, this);\ | |
| });\ | |
| ' ), | |
| /** | |
| * property.mapfn(fn, context, width) | |
| * | |
| * Create a new subordinate property of the given width and map events | |
| * from the superordinate property to the subordinate property by way of | |
| * fn. | |
| * | |
| * Experimental variation of map1 and map which uses a callback to | |
| * emit events on the subordinate property rather than the return | |
| * value. | |
| * | |
| * @param {Function} fn(emit, a, b, ..., prev_a, prev_b, ...) | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @param {Integer} width Width of the subordinate property. If left | |
| * undefined, the width is inherited. | |
| * | |
| * @returns {SubordinateStream} | |
| */ | |
| mapfn: new Function('fn', 'context', 'width', '\ | |
| "use strict";\ | |
| var superordinate_property = this;\ | |
| function mapfn__observer(' + args_list + args_list_prev_f + ') {\ | |
| fn.call(context, this.stream__mapfn_emit' + args_list_f + args_list_prev_f + ')\ | |
| }\ | |
| var property = new this.property__property_index[(width === undefined ? ' + width + ' : width)](function mapfn__open() {\ | |
| superordinate_property.on_event(mapfn__observer, this);\ | |
| }, function mapfn__close() {\ | |
| superordinate_property.off_event(mapfn__observer, this);\ | |
| });\ | |
| property.stream__mapfn_emit = function (' + args_list + ') {\ | |
| property.emit(' + args_list + ');\ | |
| };\ | |
| return property;\ | |
| ' ), | |
| /** | |
| * property.filter(fn, context, a, b, ...) | |
| * | |
| * Create a new subordinate property and re-emit events that are | |
| * accepted by the filter function. | |
| * | |
| * The a, b, ... arguments are used to initialize the subordinate | |
| * property in-case the initial value is rejected by the filter | |
| * function. | |
| * | |
| * @param {Function} fn Event filter function. Should consume | |
| * twice as many arguments as the Property's width. The first set | |
| * are the current values, the second set are the previous values. | |
| * It's return value will be cast to boolean, and if true the event | |
| * will be re-emitted. | |
| * | |
| * @param {Object} context Context in which to call fn. | |
| * | |
| * @returns {SubordinateProperty} | |
| */ | |
| filter: new function_fn_args('\ | |
| "use strict";\ | |
| var superordinate_property = this;\ | |
| function filter__observer(' + args_list + args_list_prev_f + ') {\ | |
| if(fn.call(context' + args_list_f + args_list_prev_f + ') === true) this.emit(' + args_list + ');\ | |
| };\ | |
| return new this.property__property_index[' + width + '](function filter__open() {\ | |
| superordinate_property.on_event(filter__observer, this);\ | |
| }, function filter__close() {\ | |
| superordinate_property.off_event(filter__observer, this);\ | |
| }' + args_list_f + ');\ | |
| ' ), | |
| /** | |
| * property.to_stream() | |
| * | |
| * Create a new subordinate stream, which consumes and re-emits | |
| * events. The current values are discarded. | |
| */ | |
| to_stream: new Function('\ | |
| "use strict";\ | |
| var superordinate_property = this;\ | |
| return new this.property__stream_index[' + width + '](function to_stream__open() {\ | |
| superordinate_property.on_event(this.emit, this);\ | |
| }, function to_stream__close() {\ | |
| superoridnate_property.off_event(this.emit, this);\ | |
| });\ | |
| ' ), | |
| }; | |
| return extend(prototype, shared_property_prototype); | |
| } | |
| /** | |
| * produce_propertyN_constructor(width) | |
| * | |
| * Produce a constructor function for a property of the given width. | |
| * | |
| * This does not have a proper prototype set. | |
| * | |
| * @returns {Function} | |
| */ | |
| function produce_propertyN_constructor(width) { | |
| var args = arg_names.slice(0, width); | |
| var function_constructor = Function.bind.apply(Function, [null].concat(args)); | |
| var write_values = args.reduce(function (s, a) { return s + 'this.property__' + a + ' = ' + a + ';' }, ''); | |
| /** | |
| * Property(a, b, ...) | |
| * | |
| * Construct a new Property with the initial values as supplied. | |
| * | |
| * @returns {Property} | |
| */ | |
| return new function_constructor('\ | |
| "use strict";\ | |
| this.property__observers = [];\ | |
| ' + write_values + '\ | |
| ' ); | |
| } | |
| // Prototype shared by all SubordinateProperty objects regardless of width. | |
| var shared_subordinate_property_prototype = { | |
| // See property.off_event | |
| off_event: function(fn_or_handle, ctxt) { | |
| // Decide if we're being called with a handle or a function-context | |
| // pair. | |
| var fn, context; | |
| if(fn_or_handle instanceof Array) { | |
| fn = fn_or_handle[0]; | |
| context = fn_or_handle[1]; | |
| } | |
| else { | |
| fn = fn_or_handle; | |
| context = ctxt; | |
| } | |
| // Iterate through the observers until something matches our | |
| // function-context pair, then splice it out. | |
| var observers = this.property__observers; | |
| for(var i = 0, length = observers.length; i !== length; i += 2) { | |
| if(observers[i] === fn && observers[i + 1] === context) { | |
| observers.splice(i, 2); | |
| // Close the stream, because if there was a single observer, | |
| // then the stream was open. Now that one observer has been | |
| // removed, there are no observers. | |
| if(length === 2) { | |
| this.property__active = false; | |
| this.property__close(); | |
| } | |
| break; | |
| } | |
| } | |
| }, | |
| // See property.off_event_all | |
| off_event_all: function () { | |
| this.property__observers = []; | |
| this.property__active = false; | |
| this.property__close(); | |
| }, | |
| // See property.to_property | |
| to_property: function () { | |
| return this; | |
| }, | |
| // See property.destroy | |
| destroy: null | |
| }; | |
| // For now, destroy is just an alias. | |
| shared_subordinate_property_prototype.destroy = shared_subordinate_property_prototype.off_event_all; | |
| /** | |
| * produce_subordinate_propertyN_prototype(propertyN_prototype, width) | |
| * | |
| * Produce a prototype for a SubordinateProperty of the given width. | |
| * | |
| * Because this prototype requires many of the functions provided by a | |
| * regular Property's prototype, propertyN_prototype must be provided. | |
| * | |
| * @returns {Object} | |
| */ | |
| function produce_subordinate_propertyN_prototype(propertyN_prototype, width) { | |
| var args = arg_names.slice(0, width); | |
| // The args identifiers prefixed with 'this.property__' with a comma | |
| // preceding the first identifier. Empty string if width = 0. | |
| var current_values_f = args.reduce(function (s, a) { return s + ', this.property__' + a }, ''); | |
| var prototype = { | |
| // See property.on_event | |
| on_event: new Function('fn', 'context', '\ | |
| "use strict";\ | |
| if(this.property__active === false) {\ | |
| this.property__active = true;\ | |
| this.property__open();\ | |
| }\ | |
| this.property__observers.push(fn, context);\ | |
| fn.call(context' + current_values_f + ');\ | |
| return [fn, context];\ | |
| ' ), | |
| }; | |
| return extend({}, propertyN_prototype, prototype, shared_subordinate_property_prototype); | |
| } | |
| /** | |
| * produce_subordinate_propertyN_constructor(width) | |
| * | |
| * Produce a constructor function for a SubordinateProperty of the given | |
| * width. | |
| * | |
| * This does not have a proper prototype set. | |
| * | |
| * @returns {Function} | |
| */ | |
| function produce_subordinate_propertyN_constructor(width) { | |
| var args = arg_names.slice(0, width); | |
| var function_constructor = Function.bind.apply(Function, [null, 'open', 'close'].concat(args)); | |
| var write_values = args.reduce(function (s, a) { return s + 'this.property__' + a + ' = ' + a + ';' }, ''); | |
| return new function_constructor('\ | |
| "use strict";\ | |
| this.property__observers = [];\ | |
| ' + write_values + '\ | |
| this.property__active = false;\ | |
| this.property__open = open;\ | |
| this.property__close = close;\ | |
| ' ); | |
| } | |
| // Construct multiple variants of stream and property. | |
| for(var N = 0; N !== 7; N++) { | |
| var StreamN = function () { | |
| this.stream__observers = []; | |
| }; | |
| var stream_n_prototype = produce_streamN_prototype(N) | |
| extend(StreamN.prototype, stream_n_prototype); | |
| var SubordinateStreamN = function (open, close) { | |
| this.stream__observers = []; | |
| this.stream__active = false; | |
| this.stream__open = open; | |
| this.stream__close = close; | |
| }; | |
| var subordinate_stream_n_prototype = produce_subordinate_streamN_prototype(stream_n_prototype, N); | |
| extend(SubordinateStreamN.prototype, subordinate_stream_n_prototype); | |
| var PropertyN = produce_propertyN_constructor(N); | |
| var property_n_prototype = produce_propertyN_prototype(N); | |
| extend(PropertyN.prototype, property_n_prototype); | |
| var SubordinatePropertyN = produce_subordinate_propertyN_constructor(N); | |
| var subordinate_property_n_prototype = produce_subordinate_propertyN_prototype(property_n_prototype, N); | |
| extend(SubordinatePropertyN.prototype, subordinate_property_n_prototype); | |
| exports["Stream" + N] = StreamN; | |
| exports["SubordinateStream" + N] = SubordinateStreamN; | |
| subordinate_stream_index[N] = SubordinateStreamN; | |
| exports["Property" + N] = PropertyN; | |
| exports["SubordinateProperty" + N] = SubordinatePropertyN; | |
| subordinate_property_index[N] = SubordinatePropertyN; | |
| } | |
| })(this); | |
| /** | |
| * extend(target, [ mixin, [ mixin, ... ]]) | |
| * | |
| * Copy all of the properties on each of the mixins onto the target. | |
| * | |
| * Each mixin is considered in order, properties on each mixin are copied on | |
| * to the target. Later mixins will overwrite properties provided by earlier | |
| * mixins. | |
| * | |
| * The object referenced by target is modified and returned. | |
| */ | |
| function extend(target, mixins) { | |
| var len = arguments.length; | |
| for(var i = 1; i !== len; i += 1) { | |
| var mixin = arguments[i]; | |
| for(var prop in mixin) { | |
| target[prop] = mixin[prop]; | |
| } | |
| } | |
| return target; | |
| } | |
| function log_bind() { | |
| var args = [console]; | |
| var length = arguments.length; | |
| for(var i = 0; i !== length; i += 1) args.push(arguments[i]); | |
| return console.log.bind.apply(console.log, args); | |
| } | |
| ///////// Tutorial ///////////////////////////////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| // Create a new three-argument stream | |
| var s3 = new Stream3(); | |
| // Observing a stream | |
| var handle = s3.on_event(log_bind('event handler A')); | |
| // Also observing a stream | |
| var event_handler_b = log_bind('event handler B'); | |
| s3.on_event(event_handler_b, null); | |
| // Removing an observer can be done using the handle | |
| s3.off_event(handle); | |
| // Or using the originally bound function and context | |
| s3.off_event(event_handler_b, null); | |
| s3.on_event(log_bind('event handler C')); | |
| // To emit an event: | |
| s3.emit(1, 2, 3); | |
| // Since s3 is a three-argument stream it takes a maximum of three arguments. | |
| // Extra arguments are ignored, and unbound arguments are passed as undefined. | |
| s3.emit(1); | |
| // Some higher order functions can be used: | |
| var mult_s3 = s3.mapfn(function (emit, a, b, c) { | |
| emit(a * b, b * c, c * a); | |
| }); | |
| mult_s3.on_event(log_bind('multiplication handler')); | |
| var filt_s3 = s3.filter(function (a, b, c) { | |
| return (a + b + c) % 2; | |
| }); | |
| filt_s3.on_event(log_bind('filtration handler')); | |
| // Each creates a subordinate stream which observes events and re-emits them. | |
| // But if the subordinate stream has no observers, then it doesn't observe | |
| // events: | |
| var dummy_s3 = s3.filter(function (a, b, c) { | |
| console.error('Unbound filter called!'); | |
| }); | |
| s3.emit(2, 4, 6); | |
| s3.emit(3, 5, 7); | |
| s3.emit(4, 6, 8); | |
| // Events can be manually emitted from subordinate streams: | |
| mult_s3.emit(10, 20, 30); | |
| // Properties are like streams, except they persist their last event, which they | |
| // immediately emit to each new observer. | |
| // Create a new two-argument property, and initialize it. | |
| var p2 = new Property2('Hello', 5); | |
| // They have the same interface as streams | |
| p2.on_event(log_bind('property observer A')); | |
| // Their observers, however receive the current value of the property along with | |
| // whatever their previous value was. | |
| p2.emit('Observer', 8); | |
| // An example of a filter which emits events when the length of the current | |
| // first value isn't the same as the previous second value. | |
| var filt_p2 = p2.filter(function (str, len, prev_str, prev_len) { | |
| return str.length !== prev_len; | |
| }); | |
| filt_p2.on_event(log_bind('event string length incorrect')); | |
| p2.emit('Watcher', 5); | |
| p2.emit('Child', 0); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment