Bacon.js
A functional reactive programming lib for TypeScript JavaScript, written in TypeScript.
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.
Here's the stuff.
Install and Usage
Typescript
Bacon.js starting from version 3.0 is a Typescript library so you won't need any external types. Just
Install using npm
.
npm install baconjs
Then you can
import { EventStream, once } from "baconjs"
let s: EventStream<string> = once("hello")
s.log()
As you can see, the global methods, such as once
are imported separately.
Check out the new API Documentation, that's now generated using Typedoc from the Typescript source code.
NPM, CommonJS, Node.js
If you're on to CommonJS (node.js, webpack or similar) you can install Bacon using npm.
npm install baconjs
Try it like this:
node
Bacon=require("baconjs")
Bacon.once("hello").log()
The global methods, such as once
are available in the Bacon
object.
Bower
For bower users:
bower install bacon
CDN / Script Tags
Both minified and unminified versions available on cdnjs.
So you can also include Bacon.js using
<script src="https://cdnjs.cloudflare.com/ajax/libs/bacon.js/2.0.9/Bacon.js"></script>
<script>
Bacon.once("hello").log()
</script>
AMD / require.js
Bacon.js is an UMD module so it should work with AMD/require.js too. Not tested lately though.
Github
Prefer to drink from the firehose? Download from Github master.
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 in EventStreams and dynamic values (which are called Properties in Bacon.js).
You can wrap an event source, say "mouse clicks on a DOM element" into an EventStream by saying
let $ = (selector) => document.querySelector(selector)
var clickE = Bacon.fromEvent($("h1"), "click")
The $
helper function above could be replaced with, for instance, jQuery or Zepto.
Each EventStream represents a stream of events. It is an Observable, meaning
that you can listen to events in the stream using, for instance, the onValue
method
with a callback. Like this:
clickE.onValue(() => alert("you clicked the h1 element") )
But you can do neater stuff too. The Bacon of Bacon.js is that you can transform,
filter and combine these streams in a multitude of ways (see EventStream API). The methods map
,
filter
, for example, are similar to same functions in functional list programming
(like Underscore). So, if you say
let plusE = Bacon.fromEvent($("#plus"), "click").map(1)
let minusE = Bacon.fromEvent($("#minus"), "click").map(-1)
let bothE = plusE.merge(minusE)
.. 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 bothE
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:
bothE.onValue(val => { console.log(val) })
Note that you can also use the log
method to log stream values to console
:
bothE.log()
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 cursor position as a Property. You can create Properties from
an EventStream with scan
or toProperty
methods. So, let's say
let add = (x, y) => x + y
let counterP = bothE.scan(0, add)
counterP.onValue(sum => $("#sum").textContent = sum )
The counterP
property will contain the sum of the values in the bothE
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 bothE
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.
Hiding and showing the result div depending on the content of the property value is equally straightforward
let hiddenIfZero = value => value == 0 ? "hidden" : "visible"
counterP.map(hiddenIfZero)
.onValue(visibility => { $("#sum").style.visibility = visibility })
For an actual (though a bit outdated) tutorial, please check out my blog posts
API
Creating EventStreams and Properties
There's a multitude of methods for creating an EventStream from different sources, including the DOM, node callbacks and promises for example.
See EventStream documentation.
Properties are usually created based on EventStreams. Some common ways are intruduced in Property documentation.
Combining multiple streams and properties
You can combine the latest value from multple sources using combine, combineAsArray,
combineWith or combineTemplate.
You can merge multiple streams into one using merge or mergeAll.
You can concat streams using concat or concatAll.
If you want to get the value of an observable but emit only when another stream emits an event, you might want to use sampledBy
or its cousin withLatestFrom.
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.
Event
There are essentially three kinds of Events that are emitted by EventStreams and Properties:
- Value events that convey a value. If you subscribe using onValue,
you'll only deal with values. Also
map
, filter
and most of the other operators
also deal with values only. - Error events indicate that an error has occurred. More on errors below!
- End event is emitted at most once, and is always the last event emitted by an Observable.
If you want to subscribe to all events from an Observable, you can use the subscribe method.
Errors
Error
events are always passed through all stream operators. 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 onError
.
See also mapError
, errors
, skipErrors
,
Bacon.retry
and flatMapError
.
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 operators 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 endOnError
returns a stream/property that ends immediately after the first error.
Bacon.js doesn't currently generate any Error
events itself (except when
converting errors using fromPromise
). Error
events definitely would be generated by streams derived from IO sources
such as AJAX calls.
See retry for retrying on error.
Introspection and metadata
Bacon.js provides ways to get some descriptive metadata about all Observables.
See toString
, deps
, desc
,
spy
.
Changes to earlier versions
Function Construction rules removed in 3.0
Function construction rules, which allowed you to use string shorthands for properties and methods,
were removed in version 3.0, as they are not as useful as they used to be, due to the moderd, short
lambda syntax in ES6 and Typescript, as well as libraries like Ramda and partial.lenses.
Lazy evaluation removed in 2.0
Lazy evaluation of event values has been removed in version 2.0
Cleaning up
As described above, a subscriber can signal the loss of interest in new events
in any of these two ways:
- Return
noMore
from the handler function - Call the
dispose()
function that was returned by the subscribe
or onValue
call.
Based on my experience, an actual side-effect subscriber
in application-code almost never does this. Instead you'll use methods like takeUntil
to stop listening to a source when something happens.
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
- When a Next event is emitted, the same event is emitted to all subscribers
- 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.
- 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.
- 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:
- For each new value, the subscriber function is called. The new
value is wrapped into a
Next
event. - The subscriber function returns a result which is either
noMore
or
more
. The undefined
value is handled like more
. - In case of
noMore
the source must never call the subscriber again. - When the stream ends, the subscriber function will be called with
and
End
event. The return value of the subscribe function is
ignored in this case.
A Property
behaves similarly to an EventStream
except that
- On a call to
subscribe
, it will deliver its current value
(if any) to the provided subscriber function wrapped into an Initial
event. - 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. - Property may or may not have a current value to start with. Depends
on how the Property was created.
Atomic updates
Bacon.js supports atomic updates to properties.
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
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 an 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 endOnError
to get a stream
that ends on the first 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 Typescript sources into a javascript bundle (plus typescript type definitions):
npm run dist
Result javascript files will be generated in dist
directory. If your planning
to develop Bacon.js yourself, you'll want to run [tests] too using npm test
.
You can also build a bundle with selected features only. For instance
scripts/dist 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
scripts/runtests flatmap combine takeuntil
TODO: partial testing currently broken.
Test
Run all unit tests:
npm test
The tests are run against the javascript bundle in the dist
directory. You can build the bundle using npm run dist
.
This will loop thru all files under spec
and build the library with the
single feature and run the test.
Run browser tests locally:
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: see [package.json].
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. It should work on all ES5-ish runtimes.
Automatically tested on each commit on modern browsers in Browserstack.
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
- simplicity of use
- atomic updates
If you're more into performance and less into atomic updates, you might want to check out Kefir.js!
Contribute
Use GitHub issues and Pull Requests.
Note: this readme is generated from readme-src.coffee
. After updating the src file, run npm run readme
.
Thanks to BrowserStack for kindly providing me with free of charge automatic testing time.
Thanks also to Reaktor for supporting Bacon.js development and letting me use some of my working hours on open-source development.