Skip to content

FRP (functional reactive programming) library for Javascript

License

Notifications You must be signed in to change notification settings

ryanmtaylor/bacon.js

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bacon.js

A small functional reactive programming lib for JavaScript.

Turns your event spaghetti into clean and declarative feng shui bacon, by switching from imperative to functional. It's like replacing nested for-loops with functional programming concepts like map and filter. Stop working on individual events and work with event streams instead. Combine your data with merge and combine. Then switch to the heavier weapons and wield flatMap and combineTemplate like a boss.

It's the _ of Events. Too bad the symbol ~ is not allowed in JavaScript.

Here's the stuff.

You can also check out my entertaining (LOL), interactive, solid-ass slideshow.

And remember to give me feedback on the bacon! Let me know if you've used it. Tell me how it worked for you. What's missing? What's wrong? Please contribute!

Build Status NPM version NuGet version Dependency Status devDependency Status

Table of contents

Install

If you're targeting to node.js, you can

npm install baconjs

For bower users:

bower install bacon

Both minified and unminified versions available on cdnjs.

Starting from 0.7.45, you can build your own Bacon.js bundle with selected features only. See instructions here.

Prefer to drink from the firehose? Download from Github master.

Visual Studio users can obtain version 0.7.76 via NuGet Packages https://www.nuget.org/packages/Bacon.js/0.7.76

Intro

The idea of Functional Reactive Programming is quite well described by Conal Elliot at Stack Overflow.

Bacon.js is a library for functional reactive programming. Or let's say it's a library for working with events and dynamic values (which are called Properties in Bacon.js).

Anyways, you can wrap an event source, say "mouse clicks on an element" into an EventStream by saying

var clicks = $("h1").asEventStream("click")

Each EventStream represents a stream of events. It is an Observable object, meaning that you can listen to events in the stream using, for instance, the onValue method with a callback. Like this:

clicks.onValue(function() { alert("you clicked the h1 element") })

But you can do neater stuff too. The Bacon of bacon.js is in that you can transform, filter and combine these streams in a multitude of ways (see API below). The methods map, filter, for example, are similar to same functions in functional list programming (like Underscore). So, if you say

var plus = $("#plus").asEventStream("click").map(1)
var minus = $("#minus").asEventStream("click").map(-1)
var both = plus.merge(minus)

.. you'll have a stream that will output the number 1 when the "plus" button is clicked and another stream outputting -1 when the "minus" button is clicked. The both stream will be a merged stream containing events from both the plus and minus streams. This allows you to subscribe to both streams with one handler:

both.onValue(function(val) { /* val will be 1 or -1 */ })

In addition to EventStreams, bacon.js has a thing called Property, that is almost like an EventStream, but has a "current value". So things that change and have a current state are Properties, while things that consist of discrete events are EventStreams. You could think mouse clicks as an EventStream and mouse position as a Property. You can create Properties from an EventStream with scan or toProperty methods. So, let's say

function add(x, y) { return x + y }
var counter = both.scan(0, add)
counter.onValue(function(sum) { $("#sum").text(sum) })

The counter property will contain the sum of the values in the both stream, so it's practically a counter that can be increased and decreased using the plus and minus buttons. The scan method was used here to calculate the "current sum" of events in the both stream, by giving a "seed value" 0 and an "accumulator function" add. The scan method creates a property that starts with the given seed value and on each event in the source stream applies the accumulator function to the current property value and the new value from the stream.

Properties can be very conveniently used for assigning values and attributes to DOM elements with JQuery. Here we assign the value of a property as the text of a span element whenever it changes:

property.assign($("span"), "text")

Hiding and showing the same span depending on the content of the property value is equally straightforward

function hiddenForEmptyValue(value) { return value == "" ? "hidden" : "visible" }
property.map(hiddenForEmptyValue).assign($("span"), "css", "visibility")

In the example above a property value of "hello" would be mapped to "visible", which in turn would result in Bacon calling

$("span").css("visibility", "visible")

For an actual tutorial, please check out my blog posts

API

Creating streams

$.asEventStream(eventName) creates an EventStream from events on a jQuery or Zepto.js object. You can pass optional arguments to add a jQuery live selector and/or a function that processes the jQuery event and its parameters, if given, like this:

$("#my-div").asEventStream("click", ".more-specific-selector")
$("#my-div").asEventStream("click", ".more-specific-selector", function(event, args) { return args[0] })
$("#my-div").asEventStream("click", function(event, args) { return args[0] })

Bacon.fromPromise(promise [, abort] [, eventTransformer]) creates an EventStream from a Promise object such as JQuery Ajax. This stream will contain a single value or an error, followed immediately by stream end. You can use the optional abort flag (i.e. ´fromPromise(p, true)´ to have the abort method of the given promise be called when all subscribers have been removed from the created stream. You can also pass an optional function that transforms the promise value into Events. The default is to transform the value into [new Bacon.Next(value), new Bacon.End()]. Check out this example.

Bacon.fromEvent(target, eventName [, eventTransformer]) creates an EventStream from events on a DOM EventTarget or Node.JS EventEmitter object, or an object that supports event listeners using on/off methods. You can also pass an optional function that transforms the emitted events' parameters.

Bacon.fromEvent(document.body, "click").onValue(function() { alert("Bacon!") })

Bacon.fromCallback(f [, args...]) creates an EventStream from a function that accepts a callback. The function is supposed to call its callback just once. For example:

Bacon.fromCallback(function(callback) {
  setTimeout(function() {
    callback("Bacon!")
  }, 1000)
})

This would create a stream that outputs a single value "Bacon!" and ends after that. The use of setTimeout causes the value to be delayed by 1 second.

You can also give any number of arguments to fromCallback, which will be passed to the function. These arguments can be simple variables, Bacon EventStreams or Properties. For example the following will output "Bacon rules":

bacon = Bacon.constant('bacon')
Bacon.fromCallback(function(a, b, callback) {
  callback(a + ' ' + b);
}, bacon, 'rules').log();

Bacon.fromCallback(object, methodName [, args...]) a variant of fromCallback which calls the named method of a given object.

Bacon.fromNodeCallback(f [, args...]) behaves the same way as Bacon.fromCallback, except that it expects the callback to be called in the Node.js convention: callback(error, data), where error is null if everything is fine. For example:

var Bacon = require('baconjs').Bacon,
    fs = require('fs');
var read = Bacon.fromNodeCallback(fs.readFile, 'input.txt');
read.onError(function(error) { console.log("Reading failed: " + error); });
read.onValue(function(value) { console.log("Read contents: " + value); });

Bacon.fromNodeCallback(object, methodName [, args...]) a variant of fromNodeCallback which calls the named method of a given object.

Bacon.fromPoll(interval, f) polls given function with given interval. Function should return Events: either Bacon.Next or Bacon.End. Polling occurs only when there are subscribers to the stream. Polling ends permanently when f returns Bacon.End.

Bacon.once(value) creates an EventStream that delivers the given single value for the first subscriber. The stream will end immediately after this value. You can also send an Bacon.Error event instead of a value: Bacon.once(new Bacon.Error("fail")).

Bacon.fromArray(values) creates an EventStream that delivers the given series of values (given as array) to the first subscriber. The stream ends after these values have been delivered. You can also send Bacon.Error events, or any combination of pure values and error events like this: `Bacon.fromArray([1, new Bacon.Error()])

Bacon.interval(interval, value) repeats the single element indefinitely with the given interval (in milliseconds)

Bacon.sequentially(interval, values) creates a stream containing given values (given as array). Delivered with given interval in milliseconds.

Bacon.repeatedly(interval, values) repeats given elements indefinitely with given interval in milliseconds. For example, repeatedly(10, [1,2,3]) would lead to 1,2,3,1,2,3... to be repeated indefinitely.

Bacon.repeat(fn) Calls generator function which is expected to return an observable. The returned EventStream contains values and errors from the spawned observable. When the spawned observable ends, the generator is called again to spawn a new observable.

This is repeated until the generator returns a falsy value (such as undefined or false).

The generator function is called with one argument — iteration number starting from 0.

Here's an example:

Bacon.repeat(function(i) {
  if (i < 3) {
    return Bacon.once(i);
  } else {
    return false;
  }
}).log()

The example will produce values 0, 1 and 2.

Bacon.never() creates an EventStream that immediately ends.

Bacon.later(delay, value) creates a single-element stream that produces given value after given delay (milliseconds).

new Bacon.EventStream(subscribe) creates an EventStream with the given subscribe function.

property.changes creates a stream of changes to the Property. The stream does not include an event for the current value of the Property at the time this method was called.

property.toEventStream() creates an EventStream based on this Property. The stream contains also an event for the current value of this Property at the time this method was called.

new Bacon.Bus() creates a pushable/pluggable stream (see Bus section below)

Pro tip: you can also put Errors into streams created with the constructors above, by using an Bacon.Error object instead of a plain value.

Bacon.fromBinder for custom streams

If none of the factory methods above apply, you may of course roll your own EventStream by using Bacon.fromBinder.

Bacon.fromBinder(subscribe) The parameter subscribe is a function that accepts a sink which is a function that your subscribe function can "push" events to.

For example:

var stream = Bacon.fromBinder(function(sink) {
  sink("first value")
  sink([new Bacon.Next("2nd"), new Bacon.Next("3rd")])
  sink(new Bacon.Next(function() {
    return "This one will be evaluated lazily"
  }))
  sink(new Bacon.Error("oops, an error"))
  sink(new Bacon.End())
  return function() {
     // unsub functionality here, this one's a no-op
  }
})
stream.log()

As shown in the example, you can push

  • A plain value, like "first value"
  • An Event object including Bacon.Error (wraps an error) and Bacon.End (indicates stream end).
  • An array of event objects at once

Other examples can be found on JSFiddle and the Bacon.js blog.

The subscribe function must return a function. Let's call that function unsubscribe. The returned function can be used by the subscriber (directly or indirectly) to unsubscribe from the EventStream. It should release all resources that the subscribe function reserved.

The sink function may return Bacon.noMore (as well as Bacon.more or any other value). If it returns Bacon.noMore, no further events will be consumed by the subscriber. The subscribe function may choose to clean up all resources at this point (e.g., by calling unsubscribe). This is usually not necessary, because further calls to sink are ignored, but doing so can increase performance in rare cases.

The EventStream will wrap your subscribe function so that it will only be called when the first stream listener is added, and the unsubscribe function is called only after the last listener has been removed. The subscribe-unsubscribe cycle may of course be repeated indefinitely, so prepare for multiple calls to the subscribe function.

A note about the new Bacon.Next(..) constructor: You can use it like

new Bacon.Next("value")

But the canonical way would be

new Bacon.Next(function() { return "value"; })

The former version is safe only when you know that the actual value in the stream is not a function.

The idea in using a function instead of a plain value is that the internals on Bacon.js take advantage of lazy evaluation by deferring the evaluations of values created by map, combine.

Bacon.noMore The opaque value sink function may return. See Bacon.fromBinder.

Bacon.more The opaque value sink function may return. See Bacon.fromBinder.

Common methods in EventStreams and Properties

Both EventStream and Property share the Observable interface, and hence share a lot of methods. Methods typically return observables so that methods can be chained; exceptions are noted. Common methods are listed below.

observable.subscribe(f) subscribes given handler function to event stream. Function will receive Event objects (see below). The subscribe() call returns a unsubscribe function that you can call to unsubscribe. You can also unsubscribe by returning Bacon.noMore from the handler function as a reply to an Event. stream.subscribe and property.subscribe behave similarly, except that the latter also pushes the initial value of the property, in case there is one.

observable.onValue(f) subscribes a given handler function to the observable. Function will be called for each new value. This is the simplest way to assign a side-effect to an observable. The difference to the subscribe method is that the actual stream values are received, instead of Event objects. The Function Construction rules below apply here. Just like subscribe, this method returns a function for unsubscribing. stream.onValue and property.onValue behave similarly, except that the latter also pushes the initial value of the property, in case there is one.

observable.onValues(f) like onValue, but splits the value (assuming its an array) as function arguments to f.

observable.onError(f) subscribes a callback to error events. The function will be called for each error in the stream. Just like subscribe, this method returns a function for unsubscribing.

observable.onEnd(f) subscribes a callback to stream end. The function will be called when the stream ends. Just like subscribe, this method returns a function for unsubscribing.

observable.toPromise([PromiseCtr]) returns a Promise which will be resolved with the last event coming from an Observable. The global ES6 promise implementation will be used unless a promise constructor is given. Use a shim if you need to support legacy browsers or platforms. caniuse promises.

observable.firstToPromise([PromiseCtr]) returns a Promise which will be resolved with the first event coming from an Observable. Like toPromise, the global ES6 promise implementation will be used unless a promise constructor is given.

observable.map(f) maps values using given function, returning a new stream/property. Instead of a function, you can also provide a constant value. Further, you can use a property extractor string like ".keyCode". So, if f is a string starting with a dot, the elements will be mapped to the corresponding field/function in the event value. For instance map(".keyCode") will pluck the keyCode field from the input values. If keyCode was a function, the result stream would contain the values returned by the function. The Function Construction rules below apply here. The map method, among many others, uses lazy evaluation.

stream.map(property) maps the stream events to the current value of the given property. This is equivalent to property.sampledBy(stream).

observable.mapError(f) maps errors using given function. More specifically, feeds the "error" field of the error event to the function and produces a Next event based on the return value. The Function Construction rules below apply here. You can omit the argument to produce a Next event with undefined value.

observable.errors() returns a stream containing Error events only. Same as filtering with a function that always returns false.

observable.skipErrors() skips all errors.

observable.mapEnd(f) Adds an extra Next event just before End. The value is created by calling the given function when the source stream ends. Instead of a function, a static value can be used. You can omit the argument to produce a Next event with undefined value.

observable.filter(f) filters values using given predicate function. Instead of a function, you can use a constant value (true to include all, false to exclude all) or a property extractor string (like ".isValuable") instead. Just like with map, indeed.

observable.filter(property) filters values based on the value of a property. Event will be included in output if and only if the property holds true at the time of the event.

observable.skipDuplicates(isEqual) drops consecutive equal elements. So, from [1, 2, 2, 1] you'd get [1, 2, 1]. Uses the === operator for equality checking by default. If the isEqual argument is supplied, checks by calling isEqual(oldValue, newValue). For instance, to do a deep comparison,you can use the isEqual function from underscore.js like stream.skipDuplicates(_.isEqual).

observable.take(n) takes at most n values from the stream and then ends the stream. If the stream has fewer than n values then it is unaffected. Equal to Bacon.never() if n <= 0.

observable.takeUntil(stream) takes elements from source until a Next event appears in the other stream. If other stream ends without value, it is ignored.

observable.takeWhile(f) takes while given predicate function holds true, and then ends. Function Construction rules apply.

observable.takeWhile(property) takes values while the value of a property holds true, and then ends.

observable.first() takes the first element from the stream. Essentially observable.take(1).

observable.last() takes the last element from the stream. None, if stream is empty.

Note: neverEndingStream.last() creates the stream which doesn't produce any events and never ends.

observable.skip(n) skips the first n elements from the stream

observable.delay(delay) delays the stream/property by given amount of milliseconds. Does not delay the initial value of a Property.

var delayed = source.delay(2)
source:    asdf----asdf----
delayed:   --asdf----asdf--

observable.throttle(delay) throttles stream/property by given amount of milliseconds. Events are emitted with the minimum interval of delay. The implementation is based on stream.bufferWithTime. Does not affect emitting the initial value of a Property.

Example:

var throttled = source.throttle(2)
source:    asdf----asdf----
throttled: --s--f----s--f--

observable.debounce(delay) throttles stream/property by given amount of milliseconds, but so that event is only emitted after the given "quiet period". Does not affect emitting the initial value of a Property. The difference of throttle and debounce is the same as it is in the same methods in jQuery.

Example:

source:             asdf----asdf----
source.debounce(2): -----f-------f--

observable.debounceImmediate(delay) passes the first event in the stream through, but after that, only passes events after a given number of milliseconds have passed since previous output.

Example:

source:                      asdf----asdf----
source.debounceImmediate(2): a-d-----a-d-----

observable.bufferingThrottle(minimumInterval) throttles the observable using a buffer so that at most one value event in minimumInteval is issued. Unlike throttle, it doesn't discard the excessive events but buffers them instead, outputting them with a rate of at most one value per minimumInterval.

Example:

var throttled = source.bufferingThrottle(2)
source:    asdf----asdf----
throttled: a-s-d-f-a-s-d-f-

observable.doAction(f) returns a stream/property where the function f is executed for each value, before dispatching to subscribers. This is useful for debugging, but also for stuff like calling the preventDefault() method for events. In fact, you can also use a property-extractor string instead of a function, as in ".preventDefault".

observable.doError(f) returns a stream/property where the function f is executed for each error, before dispatching to subscribers. That is, same as doAction but for errors.

observable.not() returns a stream/property that inverts boolean values

observable.flatMap(f) for each element in the source stream, spawn a new stream using the function f. Collect events from each of the spawned streams into the result EventStream. Note that instead of a function, you can provide a stream/property too. Also, the return value of function f can be either an Observable (stream/property) or a constant value. The result of flatMap is always an EventStream.

The Function Construction rules below apply here.

stream.flatMap() can be used conveniently with Bacon.once() and Bacon.never() for converting and filtering at the same time, including only some of the results.

Example - converting strings to integers, skipping empty values:

stream.flatMap(function(text) {
    return (text != "") ? parseInt(text) : Bacon.never()
})

observable.flatMapLatest(f) like flatMap, but instead of including events from all spawned streams, only includes them from the latest spawned stream. You can think this as switching from stream to stream. Note that instead of a function, you can provide a stream/property too.

The Function Construction rules below apply here.

observable.flatMapFirst(f) like flatMap, but only spawns a new stream if the previously spawned stream has ended.

The Function Construction rules below apply here.

observable.flatMapError(f) like flatMap, but is applied only on Error events. Returned values go into the value stream, unless an error event is returned. As an example, one type of error could result in a retry and another just passed through, which can be implemented using flatMapError.

observable.flatMapWithConcurrencyLimit(limit, f) a super method of flatMap family. It limits the number of open spawned streams and buffers incoming events. flatMapConcat is flatMapWithConcurrencyLimit(1) (only one input active), and flatMap is flatMapWithConcurrencyLimit ∞ (all inputs are piped to output).

The Function Construction rules below apply here.

observable.flatMapConcat(f) a flatMapWithConcurrencyLimit with limit of 1.

The Function Construction rules below apply here.

observable.scan(seed, f) scans stream/property with given seed value and accumulator function, resulting to a Property. For example, you might use zero as seed and a "plus" function as the accumulator to create an "integral" property. Instead of a function, you can also supply a method name such as ".concat", in which case this method is called on the accumulator value and the new stream value is used as argument.

Example:

var plus = function (a,b) { return a + b }
Bacon.sequentially(1, [1,2,3]).scan(0, plus)

This would result to following elements in the result stream:

seed value = 0
0 + 1 = 1
1 + 2 = 3
3 + 3 = 6

When applied to a Property as in r = p.scan(seed, f), there's a (hopefully insignificant) catch: The starting value for r depends on whether p has an initial value when scan is applied. If there's no initial value, this works identically to EventStream.scan: the seed will be the initial value of r. However, if r already has a current/initial value x, the seed won't be output as is. Instead, the initial value of r will be f(seed, x). This makes sense, because there can only be 1 initial value for a Property at a time.

observable.fold(seed, f) is like scan but only emits the final value, i.e. the value just before the observable ends. Returns a Property.

observable.reduce(seed, f) synonym for fold.

observable.diff(start, f) returns a Property that represents the result of a comparison between the previous and current value of the Observable. For the initial value of the Observable, the previous value will be the given start.

Example:

var distance = function (a,b) { return Math.abs(b - a) }
Bacon.sequentially(1, [1,2,3]).diff(0, distance)

This would result to following elements in the result stream:

1 - 0 = 1
2 - 1 = 1
3 - 2 = 1

observable.zip(other [, f]) return an EventStream with elements pair-wise lined up with events from this and the other EventStream or Property. A zipped stream will publish only when it has a value from each source and will only produce values up to when any single source ends.

The given function f is used to create the result value from value in the two sources. If no function is given, the values are zipped into an array.

Be careful not to have too much "drift" between streams. If one stream produces many more values than some other excessive buffering will occur inside the zipped observable.

Example 1:

var x = Bacon.fromArray([1, 2])
var y = Bacon.fromArray([3, 4])
x.zip(y, function(x, y) { return x + y })

# produces values 4, 6

See also zipWith and zipAsArray for zipping more than 2 sources.

observable.slidingWindow(max [, min]) returns a Property that represents a "sliding window" into the history of the values of the Observable. The result Property will have a value that is an array containing the last n values of the original observable, where n is at most the value of the max argument, and at least the value of the min argument. If the min argument is omitted, there's no lower limit of values.

For example, if you have a stream s with value a sequence 1 - 2 - 3 - 4 - 5, the respective values in s.slidingWindow(2) would be [] - [1] - [1,2] - [2,3] - [3,4] - [4,5]. The values of s.slidingWindow(2,2)would be [1,2] - [2,3] - [3,4] - [4,5].

observable.log() logs each value of the Observable to the console. It optionally takes arguments to pass to console.log() alongside each value. To assist with chaining, it returns the original Observable. Note that as a side-effect, the observable will have a constant listener and will not be garbage-collected. So, use this for debugging only and remove from production code. For example:

myStream.log("New event in myStream")

or just

myStream.log()

observable.doLog() logs each value of the Observable to the console. doLog() behaves like log but does not subscribe to the event stream. You can think of doLog() as a logger function that – unlike log() – is safe to use in production. doLog() is safe, because it does not cause the same surprising side-effects as log() does.

observable.combine(property2, f) combines the latest values of the two streams or properties using a two-arg function. Similarly to scan, you can use a method name instead, so you could do a.combine(b, ".concat") for two properties with array value. The result is a Property.

observable.withStateMachine(initState, f) lets you run a state machine on an observable. Give it an initial state object and a state transformation function that processes each incoming event and returns and array containing the next state and an array of output events. Here's an an example, where we calculate the total sum of all numbers in the stream and output the value on stream end:

Bacon.fromArray([1,2,3])
  .withStateMachine(0, function(sum, event) {
    if (event.hasValue())
      return [sum + event.value(), []]
    else if (event.isEnd())
      return [undefined, [new Bacon.Next(sum), event]]
    else
      return [sum, [event]]
  })

observable.decode(mapping) decodes input using the given mapping. Is a bit like a switch-case or the decode function in Oracle SQL. For example, the following would map the value 1 into the string "mike" and the value 2 into the value of the who property.

property.decode({1 : "mike", 2 : who})

This is actually based on combineTemplate so you can compose static and dynamic data quite freely, as in

property.decode({1 : { type: "mike" }, 2 : { type: "other", whoThen : who }})

The return value of decode is always a Property.

observable.awaiting(otherObservable) creates a Property that indicates whether observable is awaiting otherObservable, i.e. has produced a value after the latest value from otherObservable. This is handy for keeping track whether we are currently awaiting an AJAX response:

var showAjaxIndicator = ajaxRequest.awaiting(ajaxResponse)

observable.endOnError() ends the Observable on first Error event. The error is included in the output of the returned Observable.

observable.endOnError(f) ends the Observable on first Error event for which the given predicate function returns true. The error is included in the output of the returned Observable. The Function Construction rules apply, so you can do for example .endOnError(".serious").

observable.withHandler(f) lets you do more custom event handling: you get all events to your function and you can output any number of events and end the stream if you choose. For example, to send an error and end the stream in case a value is below zero:

if (event.hasValue() && event.value() < 0) {
  this.push(new Bacon.Error("Value below zero"));
  return this.push(end());
} else {
  return this.push(event);
}

Note that it's important to return the value from this.push so that the connection to the underlying stream will be closed when no more events are needed.

observable.name(newName) sets the name of the observable. Overrides the default implementation of toString and inspect. Returns itself.

observable.withDescription(param...) Sets the structured description of the observable. The toString and inspect methods use this data recursively to create a string representation for the observable. This method is probably useful for Bacon core / library / plugin development only.

For example:

var src = Bacon.once(1)
var obs = src.map(function(x) { return -x })
console.log(obs.toString())
--> Bacon.once(1).map(function)
obs.withDescription(src, "times", -1)
console.log(obs.toString())
--> Bacon.once(1).times(-1)

observable.groupBy(keyF [, limitF]) Groups stream events to new streams by keyF. Optional limitF can be provided to limit grouped stream life. Stream transformed by limitF is passed on if provided. limitF gets grouped stream and the original event causing the stream to start as parameters.

Calculator for grouped consecutive values until group is cancelled:

var events = [
  {id: 1, type: "add", val: 3 },
  {id: 2, type: "add", val: -1 },
  {id: 1, type: "add", val: 2 },
  {id: 2, type: "cancel"},
  {id: 3, type: "add", val: 2 },
  {id: 3, type: "cancel"},
  {id: 1, type: "add", val: 1 },
  {id: 1, type: "add", val: 2 },
  {id: 1, type: "cancel"}
]

function keyF(event) {
  return event.id
}

function limitF(groupedStream, groupStartingEvent) {
  var cancel = groupedStream.filter(function(x) { return x.type === "cancel"}).take(1)
  var adds = groupedStream.filter(function(x) { return x.type === "add" })
  return adds.takeUntil(cancel).map(".val")
}

Bacon.sequentially(2, events)
  .groupBy(keyF, limitF)
  .flatMap(function(groupedStream) {
    return groupedStream.fold(0, function(acc, x) { return acc + x })
  })
  .onValue(function(sum) {
    console.log(sum)
    // returns [-1, 2, 8] in an order
  })

EventStream

Bacon.EventStream a stream of events. See methods below.

stream.concat(otherStream) concatenates two streams into one stream so that it will deliver events from stream until it ends and then deliver events from otherStream. This means too that events from stream2, occurring before the end of stream will not be included in the result stream.

stream.merge(otherStream) merges two streams into one stream that delivers events from both

stream.holdWhen(valve) pauses and buffers the event stream if last event in valve is truthy. All buffered events are released when valve becomes falsy.

stream.startWith(value) adds a starting value to the stream, i.e. concats a single-element stream contains value with this stream.

stream.skipWhile(f) skips elements until the given predicate function returns falsy once, and then lets all events pass through. The Function Construction rules below apply here.

stream.skipWhile(property) skips elements until the value of the given Property is falsy once, and then lets all events pass through.

stream.skipUntil(stream2) skips elements from stream until a Next event appears in stream2. In other words, starts delivering values from stream after first event appears in stream2.

stream.bufferWithTime(delay) buffers stream events with given delay. The buffer is flushed at most once in the given delay. So, if your input contains [1,2,3,4,5,6,7], then you might get two events containing [1,2,3,4] and [5,6,7] respectively, given that the flush occurs between numbers 4 and 5.

stream.bufferWithTime(f) works with a given "defer-function" instead of a delay. Here's a simple example, which is equivalent to stream.bufferWithTime(10):

stream.bufferWithTime(function(f) { setTimeout(f, 10) })

stream.bufferWithCount(count) buffers stream events with given count. The buffer is flushed when it contains the given number of elements. So, if you buffer a stream of [1, 2, 3, 4, 5] with count 2, you'll get output events with values [1, 2], [3, 4] and [5].

stream.bufferWithTimeOrCount(delay, count) buffers stream events and flushes when either the buffer contains the given number elements or the given amount of milliseconds has passed since last buffered event.

stream.toProperty() creates a Property based on the EventStream. Without arguments, you'll get a Property without an initial value. The Property will get its first actual value from the stream, and after that it'll always have a current value.

stream.toProperty(initialValue) creates a Property based on the EventStream with the given initial value that will be used as the current value until the first value comes from the stream.

Property

Bacon.Property a reactive property. Has the concept of "current value". You can create a Property from an EventStream by using either toProperty or scan method. Note: depending on how a Property is created, it may or may not have an initial value. The current value stays as its last value after the stream has ended.

Bacon.constant(x) creates a constant property with value x.

property.assign(obj, method [, param...]) calls the method of the given object with each value of this Property. You can optionally supply arguments which will be used as the first arguments of the method call. For instance, if you want to assign your Property to the "disabled" attribute of a JQuery object, you can do this:

myProperty.assign($("#my-button"), "attr", "disabled")

A simpler example would be to toggle the visibility of an element based on a Property:

myProperty.assign($("#my-button"), "toggle")

Note that the assign method is actually just a synonym for onValue and the function construction rules below apply to both.

property.sample(interval) creates an EventStream by sampling the property value at given interval (in milliseconds)

property.sampledBy(stream) creates an EventStream by sampling the property value at each event from the given stream. The result EventStream will contain the property value at each event in the source stream.

property.sampledBy(property) creates a Property by sampling the property value at each event from the given property. The result Property will contain the property value at each event in the source property.

property.sampledBy(streamOrProperty, f) samples the property on stream events. The result values will be formed using the given function f(propertyValue, samplerValue). You can use a method name (such as ".concat") instead of a function too.

property.changes() returns an EventStream of property value changes. Returns exactly the same events as the property itself, except any Initial events. Note that property.changes() does NOT skip duplicate values, use .skipDuplicates() for that.

property.and(other) combines properties with the && operator.

property.or(other) combines properties with the || operator.

property.startWith(value) adds an initial "default" value for the Property. If the Property doesn't have an initial value of it's own, the given value will be used as the initial value. If the property has an initial value of its own, the given value will be ignored.

Combining multiple streams and properties

Bacon.combineAsArray(streams) combines Properties, EventStreams and constant values so that the result Property will have an array of all property values as its value. The input array may contain both Properties and EventStreams. In the latter case, the stream is first converted into a Property and then combined with the other properties.

Bacon.combineAsArray(s1, s2...) just like above, but with streams provided as a list of arguments as opposed to a single array.

property = Bacon.constant(1)
stream = Bacon.once(2)
constant = 3
Bacon.combineAsArray(property, stream, constant)
# produces the value [1,2,3]

Bacon.combineWith(f, stream1, stream2...) combines given n Properties, EventStreams and constant values using the given n-ary function f(v1, v2 ...). To calculate the current sum of three numeric Properties, you can do

function sum3(x,y,z) { return x + y + z }
Bacon.combineWith(sum3, p1, p2, p3)

Bacon.combineWith(f, streams) like above, but with streams provided as a single array as opposed to a list of arguments.

streams = [Bacon.constant(1), Bacon.constant(2)]
Bacon.combineWith(Math.max, streams)

Bacon.combineWith(streams, f) like above

Bacon.combineWith(stream1, stream2..., f) like above

Bacon.combineTemplate(template) combines Properties, EventStreams and constant values using a template object. For instance, assuming you've got streams or properties named password, username, firstname and lastname, you can do

var password, username, firstname, lastname; // <- properties or streams
var loginInfo = Bacon.combineTemplate({
    magicNumber: 3,
    userid: username,
    passwd: password,
    name: { first: firstname, last: lastname }})

.. and your new loginInfo property will combine values from all these streams using that template, whenever any of the streams/properties get a new value. For instance, it could yield a value such as

{ magicNumber: 3,
  userid: "juha",
  passwd: "easy",
  name : { first: "juha", last: "paananen" }}

In addition to combining data from streams, you can include constant values in your templates.

Note that all Bacon.combine* methods produce a Property instead of an EventStream. If you need the result as an EventStream you might want to use property.changes()

Bacon.combineWith(function(v1,v2) { .. }, stream1, stream2).changes()

Bacon.mergeAll(streams) merges given array of EventStreams. Bacon.mergeAll(stream1, stream2 ...) merges given EventStreams.

Bacon.zipAsArray(streams) zips the array of EventStreams / Properties in to a new EventStream that will have an array of values from each source as its value. Zipping means that events from each source are combined pairwise so that the 1st event from each source is published first, then the 2nd event from each. The results will be published as soon as there is a value from each source.

Be careful not to have too much "drift" between streams. If one stream produces many more values than some other excessive buffering will occur inside the zipped observable.

Example:

x = Bacon.fromArray([1,2,3])
y = Bacon.fromArray([10, 20, 30])
z = Bacon.fromArray([100, 200, 300])
Bacon.zipAsArray(x, y, z)

# produces values [1, 10, 100], [2, 20, 200] and [3, 30, 300]

Bacon.zipAsArray(stream1, stream2...) just like above, but with sources provided as a list of arguments as opposed to a single array.

Bacon.zipWith(streams, f) like zipAsArray but uses the given n-ary function to combine the n values from n sources, instead of returning them in an Array.

Bacon.zipWith(f, streams) like zipAsArray but uses the given n-ary function to combine the n values from n sources, instead of returning them in an Array.

Bacon.zipWith(f, stream1, stream1...) like above

Bacon.zipWith(stream1, stream1..., f) like above

Bacon.onValues(a, b [, c...], f) is a shorthand for combining multiple sources (streams, properties, constants) as array and assigning the side-effect function f for the values. The following example would log the number 3.

function f(a, b) { console.log(a + b) }
Bacon.onValues(Bacon.constant(1), Bacon.constant(2), f)

Function Construction rules

Many methods in Bacon have a single function as their argument. Many of these actually accept a wider range of different arguments that they use for constructing the function.

Here are the different forms you can use, with examples. The basic form would be

stream.map(f) maps values using the function f(x)

As an extension to the basic form, you can use partial application:

stream.map(f, "bacon") maps values using the function f(x, y), using "bacon" as the first argument, and stream value as the second argument.

stream.map(f, "pow", "smack") maps values using the function f(x, y, z), using "pow" and "smack" as the first two arguments and stream value as the third argument.

Then, you can create method calls like this:

stream.onValue(object, method) calls the method having the given name, with stream value as the argument.

titleText.onValue($("#title"), "text") which would call the "text" method of the jQuery object matching to the HTML element with the id "title"

disableButton.onValue($("#send"), "attr", "disabled") which would call the attr method of the #send element, with "disabled" as the first argument. So if your property has the value true, it would call $("#send").attr("disabled", true)

You can call methods or return field values using a "property extractor" syntax. With this syntax, Bacon checks the type of the field and if it's indeed a method, it calls it. Otherwise it just returns field value. For example:

stream.map(".length") would return the value of the "length" field of stream values. Would make sense for a stream of arrays. So, you'd get 2 for ["cat", "dog"]

stream.map(".stuffs.length") would pick the length of the "stuffs" array that is a field in the stream value. For example, you'd get 2 for { stuffs : ["thing", "object"] }

stream.map(".dudes.1") would pick the second object from the nested "dudes" array. For example, you'd get "jack" for { dudes : ["john", "jack"] }.

stream.doAction(".preventDefault") would call the "preventDefault" method of stream values.

stream.filter(".attr", "disabled").not() would call .attr("disabled") on stream values and filter by the return value. This would practically inlude only disabled jQuery elements to the result stream.

If none of the above applies, Bacon will return a constant value. For instance:

mouseClicks.map({ isMouseClick: true }) would map all events to the object { isMouseClick: true }

Methods that support function construction include at least onValue, onError, onEnd, map, filter, assign, takeWhile, mapError and doAction.

Lazy evaluation

Methods such as map and the combine use lazy evaluation to avoid evaluating values that aren't actually needed. This can be generally considered a Good Thing, but it has it's pitfalls.

If you pass a function that referentially transparent, you'll be fine. This means that your function should return the same value regardless of when it's called.

On the other hand, if you pass a function that returns a value depending on time, you may have problems. Consider a property contents that's derived from events like below.

var items = clicks.map(getCurrentValueFromUI).toProperty()
var submittedItems = items.sampledBy(submitClick)

Now the submittedItems stream will produce the current value of the items property when an event occurs in the submitClick stream. Or so you'd think. In fact, the value of submittedItems is evaluated at the time of the event in the submitClick stream, which means that it will actually produce the value of getCurrentValueFromUI at that time, instead of at the time of the original click event.

To force evaluation at the time of original event, you can just use flatMap instead of map. As in here.

var items = clicks.flatMap(getCurrentValueFromUI).toProperty()

Latest value of Property or EventStream

One of the common first questions people ask is "how do I get the latest value of a stream or a property". There is no getLatestValue method available and will not be either. You get the value by subscribing to the stream/property and handling the values in your callback. If you need the value of more than one source, use one of the combine methods.

Bus

Bus is an EventStream that allows you to push values into the stream. It also allows plugging other streams into the Bus. The Bus practically merges all plugged-in streams and the values pushed using the push method.

new Bacon.Bus() returns a new Bus.

bus.push(x) pushes the given value to the stream.

bus.end() ends the stream. Sends an End event to all subscribers. After this call, there'll be no more events to the subscribers. Also, the bus.push and bus.plug methods have no effect.

bus.error(e) sends an Error with given message to all subscribers

bus.plug(stream) plugs the given stream to the Bus. All events from the given stream will be delivered to the subscribers of the Bus. Returns a function that can be used to unplug the same stream.

The plug method practically allows you to merge in other streams after the creation of the Bus. I've found Bus quite useful as an event broadcast mechanism in the Worzone game, for instance.

Event

Bacon.Event has subclasses Bacon.Next, Bacon.End, Bacon.Error and Bacon.Initial

Bacon.Next next value in an EventStream or a Property. Call isNext() to distinguish a Next event from other events.

Bacon.End an end-of-stream event of EventStream or Property. Call isEnd() to distinguish an End from other events.

Bacon.Error an error event. Call isError() to distinguish these events in your subscriber, or use onError to react to error events only. errorEvent.error returns the associated error object (usually string).

Bacon.Initial the initial (current) value of a Property. Call isInitial() to distinguish from other events. Only sent immediately after subscription to a Property.

Event properties and methods

event.value() returns the value associated with a Next or Initial event

event.hasValue() returns true for events of type Initial and Next

event.isNext() true for Next events

event.isInitial() true for Initial events

event.isEnd() true for End events

Errors

Bacon.Error events are always passed through all stream combinators. So, even if you filter all values out, the error events will pass through. If you use flatMap, the result stream will contain Error events from the source as well as all the spawned stream.

You can take action on errors by using the observable.onError(f) callback.

See documentation on onError, mapError, errors, skipErrors, Bacon.retry and flatMapError above.

In case you want to convert (some) value events into Error events, you may use flatMap like this:

stream = Bacon.fromArray([1,2,3,4]).flatMap(function(x) {
  if (x > 2)
    return new Bacon.Error("too big")
  else
    return x
})

Conversely, if you want to convert some Error events into value events, you may use flatMapError:

myStream.flatMapError(function(error) {
  return isNonCriticalError(error) ? handleNonCriticalError(error) : new Bacon.Error(error)
})

Note also that Bacon.js combinators do not catch errors that are thrown. Especially map doesn't do so. If you want to map things and wrap caught errors into Error events, you can do the following:

wrapped = source.flatMap(Bacon.try(dangerousOperation))

For example, you can use Bacon.try to handle JSON parse errors:

var jsonStream = Bacon
  .once('{"this is invalid json"')
  .flatMap(Bacon.try(JSON.parse))

jsonStream.onError(function(err) {
  console.error("Failed to parse JSON", err)
})

An Error does not terminate the stream. The method observable.endOnError() returns a stream/property that ends immediately after first error.

Bacon.js doesn't currently generate any Error events itself (except when converting errors using Bacon.fromPromise). Error events definitely would be generated by streams derived from IO sources such as AJAX calls.

Bacon.retry(options) is used to retry the call when there is an Error event in the stream produced by the source function.

The two required option parameters are:

  • source, a function that produces an Observable.
  • retries, the number of times to retry the source function in addition to the initial attempt.

Additionally, one may pass in one or both of the following callbacks:

  • isRetryable, a function returning true to continue retrying, false to stop. Defaults to true. The error that occurred is given as a parameter. For example, there is usually no reason to retry a 404 HTTP error, whereas a 500 or a timeout might work on the next attempt.
  • delay, a function that returns the time in milliseconds to wait before retrying. Defaults to 0. The function is given a context object with the keys error (the error that occurred) and retriesDone (the number of retries already performed) to help determine the appropriate delay e.g. for an incremental backoff.
var triggeringStream, ajaxCall // <- ajaxCall gives Errors on network or server errors
ajaxResult = triggeringStream.flatMap(function(url) {
    return Bacon.retry({
        source: function() { return ajaxCall(url) },
        retries: 5,
        isRetryable: function (error) { return error.httpStatusCode !== 404; },
        delay: function(context) { return 100; } // Just use the same delay always
    })
})

Join Patterns

Join patterns are a generalization of the zip function. While zip synchronizes events from multiple streams pairwse, join patterns allow for implementation of more advanced synchronization patterns. Bacon.js uses the Bacon.when function to convert a list of synchronization patterns into a resulting eventstream.

Bacon.when Consider implementing a game with discrete time ticks. We want to handle key-events synchronized on tick-events, with at most one key event handled per tick. If there are no key events, we want to just process a tick.

  Bacon.when(
    [tick, keyEvent], function(_, k) { handleKeyEvent(k); return handleTick(); },
    [tick], handleTick)

Order is important here. If the [tick] patterns had been written first, this would have been tried first, and preferred at each tick.

Join patterns are indeed a generalization of zip, and for EventStreams, zip is equivalent to a single-rule join pattern. The following observables have the same output, assuming that all sources are EventStreams.

Bacon.zipWith(a,b,c, combine)
Bacon.when([a,b,c], combine)

Note that Bacon.when does not trigger updates for events from Properties though; if you use a Property in your pattern, its value will be just sampled when all the other sources (EventStreams) have a value. This is useful when you need a value of a Property in your calculations. If you want your pattern to fire for a Property too, you can convert it into an EventStream using property.changes() or property.toEventStream()

Bacon.update creates a Property from an initial value and updates the value based on multiple inputs. The inputs are defined similarly to Bacon.when, like this:

var result = Bacon.update(
  initial,
  [x,y,z], function(previous,x,y,z) { ... },
  [x,y],   function(previous,x,y) { ... })

As input, each function above will get the previous value of the result Property, along with values from the listed Observables. The value returned by the function will be used as the next value of result.

Just like in Bacon.when, only EventStreams will trigger an update, while Properties will be just sampled. So, if you list a single EventStream and several Properties, the value will be updated only when an event occurs in the EventStream.

Here's a simple gaming example:

var scoreMultiplier = Bacon.constant(1)
var hitUfo = new Bacon.Bus()
var hitMotherShip = new Bacon.Bus()
var score = Bacon.update(
  0,
  [hitUfo, scoreMultiplier], function(score, _, multiplier) { return score + 100 * multiplier },
  [hitMotherShip], function(score, _) { return score + 2000 }
)

In the example, the score property is updated when either hitUfo or hitMotherShip occur. The scoreMultiplier Property is sampled to take multiplier into account when hitUfo occurs.

Join patterns as a "chemical machine"

A quick way to get some intuition for join patterns is to understand them through an analogy in terms of atoms and molecules. A join pattern can here be regarded as a recipe for a chemical reaction. Lets say we have observables oxygen, carbon and hydrogen, where an event in these spawns an 'atom' of that type into a mixture.

We can state reactions

make_water              = function(oxygen, hydrogen, hydrogen)  { /* ... consume oxygen and hydrogen ... */ }
make_carbon_monoxide    = function(oxygen, carbon)              { /* ... consume oxygen and carbon ... */ }

Bacon.when(
  [oxygen, hydrogen, hydrogen], make_water,
  [oxygen, carbon],             make_carbon_monoxide,
)

Now, every time a new 'atom' is spawned from one of the observables, this atom is added to the mixture. If at any time there are two hydrogen atoms, and an oxygen atom, the corresponding atoms are consumed, and output is produced via make_water.

The same semantics apply for the second rule to create carbon monoxide. The rules are tried at each point from top to bottom.

Join patterns and properties

Properties are not part of the synchronization pattern, but are instead just sampled. The following example take three input streams $price, $quantity and $total, e.g. coming from input fields, and defines mutally recursive behaviours in properties price, quantity and total such that

  • updating price sets total to price * quantity
  • updating quantity sets total to price * quantity
  • updating total sets price to total / quantity
  var $price, $total, $quantity = ...

  var quantity = $quantity.toProperty(1)

  var price = Bacon.when(
    [$price], id,
    [$total, quantity], function(x,y) { return x/y })
   .toProperty(0)

  var total = Bacon.when(
    [$total], id,
    [$price, quantity], function(x,y) { return x*y },
    [price, $quantity], function(x,y) { return x*y })
   .toProperty(0)

Join patterns and Bacon.bus

The result functions of join patterns are allowed to push values onto a Bus that may in turn be in one of its patterns. For instance, an implementation of the dining philosophers problem can be written as follows. (http://en.wikipedia.org/wiki/Dining_philosophers_problem)

Example:

// availability of chopsticks are implemented using Bus
var chopsticks = [new Bacon.Bus(), new Bacon.Bus(), new Bacon.Bus()]

// hungry could be any type of observable, but we'll use bus here
var hungry     = [new Bacon.Bus(), new Bacon.Bus(), new Bacon.Bus()]

// a philosopher eats for one second, then makes the chopsticks
// available again by pushing values onto their bus.
var eat = function(i) {
  return function() {
    setTimeout(function() {
      console.log('done!')
      chopsticks[i].push({})
      chopsticks[(i+1) % 3].push({})
    }, 1000);
    return 'philosopher ' + i + ' eating'
  }
}

// we use Bacon.when to make sure a hungry philosopher can eat only
// when both his chopsticks are available.
var dining = Bacon.when(
  [hungry[0], chopsticks[0], chopsticks[1]],  eat(0),
  [hungry[1], chopsticks[1], chopsticks[2]],  eat(1),
  [hungry[2], chopsticks[2], chopsticks[0]],  eat(2))

dining.log()

// make all chopsticks initially available
chopsticks[0].push({}); chopsticks[1].push({}); chopsticks[2].push({})

// make philosophers hungry in some way, in this case we just push to their bus
for (var i = 0; i < 3; i++) {
  hungry[0].push({}); hungry[1].push({}); hungry[2].push({})
}

Cleaning up

As described above, a subscriber can signal the loss of interest in new events in any of these two ways:

  1. Return Bacon.noMore from the handler function
  2. Call the dispose() function that was returned by the subscribe() call.

Based on my experience on RxJs coding, an actual side-effect subscriber in application-code never does this. So the business of unsubscribing is mostly internal business and you can ignore it unless you're working on a custom stream implementation or a stream combinator. In that case, I welcome you to contribute your stuff to bacon.js.

EventStream and Property semantics

The state of an EventStream can be defined as (t, os) where t is time and os the list of current subscribers. This state should define the behavior of the stream in the sense that

  1. When a Next event is emitted, the same event is emitted to all subscribers
  2. After an event has been emitted, it will never be emitted again, even if a new subscriber is registered. A new event with the same value may of course be emitted later.
  3. When a new subscriber is registered, it will get exactly the same events as the other subscriber, after registration. This means that the stream cannot emit any "initial" events to the new subscriber, unless it emits them to all of its subscribers.
  4. A stream must never emit any other events after End (not even another End)

The rules are deliberately redundant, explaining the constraints from different perspectives. The contract between an EventStream and its subscriber is as follows:

  1. For each new value, the subscriber function is called. The new value is wrapped into a Next event.
  2. The subscriber function returns a result which is either Bacon.noMore or Bacon.more. The undefined value is handled like Bacon.more.
  3. In case of Bacon.noMore the source must never call the subscriber again.
  4. When the stream ends, the subscriber function will be called with and Bacon.End event. The return value of the subscribe function is ignored in this case.

A Property behaves similarly to an EventStream except that

  1. On a call to subscribe, it will deliver its current value (if any) to the provided subscriber function wrapped into an Initial event.
  2. This means that if the Property has previously emitted the value x to its subscribers and that is the latest value emitted, it will deliver this value to the new subscriber.
  3. Property may or may not have a current value to start with. Depends on how the Property was created.

Atomic updates

From version 0.4.0, Bacon.js supports atomic updates to properties, with known limitations.

Assume you have properties A and B and property C = A + B. Assume that both A and B depend on D, so that when D changes, both A and B will change too.

When D changes d1 -> d2, the value of A a1 -> a2 and B changes b1 -> b2 simultaneously, you'd like C to update atomically so that it would go directly a1+b1 -> a2+b2. And, in fact, it does exactly that. Prior to version 0.4.0, C would have an additional transitional state like a1+b1 -> a2+b1 -> a2+b2

Atomic updates are limited to Properties only, meaning that simultaneous events in EventStreams will not be recognized as simultaneous and may cause extra transitional states to Properties. But as long as you're just combining Properties, you'll updates will be atomic.

For RxJs Users

Bacon.js is quite similar to RxJs, so it should be pretty easy to pick up. The major difference is that in bacon, there are two distinct kinds of Observables: the EventStream and the Property. The former is for discrete events while the latter is for observable properties that have the concept of "current value".

Also, there are no "cold observables", which means also that all EventStreams and Properties are consistent among subscribers: when as event occurs, all subscribers will observe the same event. If you're experienced with RxJs, you've probably bumped into some wtf's related to cold observables and inconsistent output from streams constructed using scan and startWith. None of that will happen with bacon.js.

Error handling is also a bit different: the Error event does not terminate a stream. So, a stream may contain multiple errors. To me, this makes more sense than always terminating the stream on error; this way the application developer has more direct control over error handling. You can always use stream.endOnError() to get a stream that ends on error!

Examples

See Examples

See Specs

See Worzone demo and source

Build

First check out the Bacon.js repository and run npm install.

Then build the coffeescript sources into javascript:

./build

Result javascript files will be generated in dist directory. If your planning to develop Bacon.js yourself, you'll want to run [tests] too.

You can also build a bundle with selected features only. For instance

./build flatmap combine takeuntil

The build system will do its best to determine the dependencies of the selected features and include those into the bundle too. You can also test the integrity of the bundle with your selected features using

./test flatmap combine takeuntil

Test

Run all unit tests:

./test

Run limited set of unit tests:

./test core _ frompromise

The names correspond to the file names under spec/specs. The library will be built with the listed features only.

You can also test all features individually:

./test-individually.js

This will loop thru all files under spec/specs and build the library with the single feature and run the test.

Run browser tests (using testem):

npm install
npm install -g testem
testem

Run browser (without testem):

npm install
browsertest/browserify
open browsertest/mocha.runner.html

Run performance tests:

performance/PerformanceTest.coffee
performance/PerformanceTest.coffee flatmap

Run memory usage tests:

coffee --nodejs '--expose-gc' performance/MemoryTest.coffee

Dependencies

Runtime: jQuery or Zepto.js (optional; just for jQ/Zepto bindings) Build/test: node.js, npm, coffeescript

Compatibility with other libs

Bacon.js doesn't mess with prototypes or the global object. Only exceptions below.

  • It exports the Bacon object, except in Node.js. In a browser, this is added to the window object.
  • If jQuery is defined, it adds the asEventStream method to jQuery (similarly to Zepto)

So, it should be pretty much compatible and a nice citizen.

I'm not sure how it works in case some other lib adds stuff to, say, Array prototype, though. Maybe add test for this later?

Compatibility with browsers

TLDR: good.

Bacon.js is not browser dependent, because it is not a UI library.

I have personally used it Bacon.js with Chrome, Firefox, Safari, IE 6+, iPhone, iPad.

Automatically tested on each commit on modern browsers and IE6+.

The full Bacon.js test suite is run on testling.ci with a wide range of browsers:

browser support test report

Results from those tests are quite unreliable, producing random failures, but the bottom line is that there are no outstanding compatibility issues.

Node.js

Sure. Works. Try it out.

npm install baconjs

Then type node and try the following

Bacon = require("baconjs").Bacon
Bacon.sequentially(1000, ["B", "A", "C", "O", "N"]).log()

AMD

Yep. Currently exports Bacon through AMD and assigns to window for backwards compatibility.

If you would like to use it with jQuery and AMD, you should monkey patch jQuery explicitly so that module loading order does not matter

define(function (require) {
    var $ = require('jquery'),
        Bacon = require('Bacon');

    $.fn.asEventStream = Bacon.$.asEventStream;

    $(document).asEventStream('click').onValue(function (e) {
        console.log(e.clientX + ', ' + e.clientY);
    });
});

Why Bacon?

Bacon.js exists largely because I got frustrated with RxJs, which is a good library, but at that time didn't have very good documentation and wasn't open-source. Things have improved a lot in the Rx world since that. Yet, there are still compelling reasons to use Bacon.js instead. Like, for instance, more consistent stream/property behavior and (arguably) simplicity of use.

Contribute

Use GitHub issues and Pull Requests.

Note: this readme is generated from readme-src.coffee. After updating the src file, run grunt readme.

Sponsors

Thanks to BrowserStack for kindly providing me with free of charge automatic testing time.

About

FRP (functional reactive programming) library for Javascript

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • CoffeeScript 54.7%
  • JavaScript 44.6%
  • Other 0.7%