Skip to content

Commit

Permalink
Emit on change, deep copy objects
Browse files Browse the repository at this point in the history
* Only emit on change
* Deep copy all objects on input and output
* Add keypath access
* Fix nested `.wait()`
  • Loading branch information
jarofghosts committed Dec 29, 2014
1 parent 6bc45e1 commit 180f07f
Show file tree
Hide file tree
Showing 4 changed files with 425 additions and 366 deletions.
167 changes: 77 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,148 +3,135 @@
[![Build Status](http://img.shields.io/travis/urbanairship/objectstate/master.svg?style=flat)](https://travis-ci.org/urbanairship/objectstate)
[![npm install](http://img.shields.io/npm/dm/objectstate.svg?style=flat)](https://www.npmjs.org/package/objectstate)

## Overview

`objectstate` exports a function that constructs a stream. The stream is
designed to be the source of truth for the state of a system. It represents
state as a JavaScript object, and updates it by 'listening' to event emitters.
When any emitter emits new values, ObjectState updates its state
state as a JavaScript object, and updates it by 'listening' to streams or
event emitters.

When any stream or event emitter emits new values, ObjectState updates its state
representation, and fires a "data" event. Client code can subscribe to the
"data" event for a full representation of the state of the system each time
it changes.

## Example

```javascript
var EE = require('events').EventEmitter

var ObjectState = require('objectstate')
var objectState = require('objectstate')
, through = require('through')

var stream = through()

var ee1 = new EE
, ee2 = new EE

var os = new ObjectState
var os = objectState()

os.listen(ee1, 'data', ['cat', 'dog'])
.listen(ee2, 'error', ['hat'])
os.listen(stream, 'rat')
.listenOn(ee1, 'data', ['cat', 'dog'])
.listenOn(ee2, 'error', ['hat'])

os.on('data', function(state) { console.log(state) })

ee1.emit('data', 1) // {"cat":1}
ee1.emit('data', 1, 2) // {"cat":1, "dog":2}
stream.queue(5) // {"rat": 5}
ee1.emit('data', 1) // {"rat": 5, "cat":1}
ee1.emit('data', 1, 2) // {"rat": 5, "cat":1, "dog":2}
ee2.emit('data', 100) // does not log, since os does not listen to ee2's
// data event.
ee2.emit('error', "hello") // {"cat":1, "dog":2, "hat":"hello"}
ee1.emit('data') // {"hat":"hello"}
stream.queue(5) // does not log, because this changes nothing.
ee2.emit('error', "hello") // {"rat": 5, "cat":1, "dog":2, "hat":"hello"}
ee1.emit('data') // {"rat": 5, "hat":"hello"}
```

## API
## Notes

ObjectState will never alter any object that is passed to it, instead it makes a
deep copy for use internally. Likewise, it only ever emits a deep copy of its
state in order to avoid outside mutation.

For performance reasons, deep copy is implemented using
`JSON.parse(JSON.stringify(state))`, which has a few limitations.

1. It will throw an error if there is a self-reference,
2. It will have strange side effects on properties that are not `null`,
`string`, `boolean`, `array`, or a plain JavaScript `object`.
This includes functions, typed arrays, and objects with prototype other than
`Object.prototype`.

`ObjectState = require('modules/objectstate')`
In practice, these limitations are mostly inconsequential. ObjectState is meant
to manage **data**, not complicated instances.

Returns a function constructor.
ObjectState only ever emits when its internal state changes, a condition
determined via a deep comparison of the new state object versus the previous
one.

`ObjectState(_initial) -> Stream`
ObjectState speaks "keypaths", meaning you can reference deeply nested
properties by dot-separated strings. For example, given the object
`{animals: {cats: {sound: 'meow'}}}`, `'animals.cats.sound'` would refer to the
string `'meow'`. If you do not need deep property access, a regular string will
work as you would expect.

## API

`objectState(_initial) -> DuplexStream`

Optionally takes an initial state (meaning an object), and returns an
ObjectState instance.

ObjectState instances are readable/writable streams. ObjectState emits whenever
its internal state changes, and can be written to with an object to set its
state.
internal state.

### Instance Methods

#### `os.listen(ee, eventName, attributes)`

- `ee`: An event emitter
#### `os.listen(stream, keypath) -> os`
- `stream`: A stream
- `keypath`: The keypath to update on emit

- `eventName`: When `ee` emits events named `eventName`, their objects are
recorded on the internal state object.
#### `os.listenOn(ee, eventName, keypaths) -> os`
- `ee`: An event emitter
- `eventName`: When `ee` emits events named `eventName`, their objects are
recorded on the internal state object.
- `keypaths`: An array of keypaths.

- `attributes`: An array of attribute names.

When the `ee` emits the event, `eventName`, `ObjectState` saves each
argument passed to the event handler under the `attributes` array element at
the same index. The i-th argument passed to the listener is saved under the
i-th element of attributes: `state[attributes[i]] = arguments[i]`.
When the `ee` emits the event `eventName`, `ObjectState` saves each argument
passed to the event handler under the `keypaths` array element at the same
index. The Nth argument passed to the listener is saved under the Nth element of
keypaths: `state[keypaths[N]] = arguments[N]`.

If an emitted argument is undefined, `ObjectState` deletes
the corresponding attribute in its internal state object.
If an emitted argument is undefined, `ObjectState` deletes the corresponding
keypath in its internal state object.

- returns itself.
If a specified parameter is falsey, it is skipped over during assignment.

#### `os.get(attr)`
#### `os.get(keypath)`

Returns the value for state attribute `attr` (or `undefined` if not set)
Returns the value for state keypath `keypath` (or `undefined` if not set).

#### `os.set(attr, value)`
#### `os.set(keypath, value)`

Set the value for attribute `attr` to `value` on the object state.
Set the value for keypath `keypath` to `value` on the state.

#### `os.remove(attr)`
#### `os.remove(keypath)`

Delete the attribute `attr` from the object state.
Delete the keypath `keypath` from the state.

#### `os.emitState()`

Emit the current state as a `data` event.

#### `os.wait(fn)`

`fn` is a function that is immediately called. All changes that happen
during its execution will be collected into a single `data` event, if the state
changed at all.

#### `os.copy()`

Returns a shallow copy of the instance's current state.

#### `os.deepcopy()`

Returns a deep copy of the instance's current state.

Deep copy is implemented using `JSON.parse(JSON.stringify(state))` which has
a few limitations. It will throw an error if there is a self-reference,
and will have strange side effects on properties that are not `null`, `strings`,
`booleans`, `arrays`, or plain JavaScript `objects`. This includes functions,
typed arrays, and objects with prototype other than `Object.prototype`.

#### `os.snapshot(deep=true)`

**WARNING** This method is deprecated and should not be used. It is scheduled to
be removed in the next major version.

Returns a function that can be used to reset the object's state. Calling the
returned restore function will emit a `data` event.

`deep` specifies whether to shallow-copy or deep-copy the current state.

if the snapshot uses a deep copy, it is subject to the same limitations as
os.deepcopy.

Usage:

```javascript
os = new ObjectState
restore = os.snapshot()
// time passes...
restore()
```

#### `os.include(otherOS)`

**WARNING** This method is deprecated and should not be used. It is scheduled to
be removed in the next major version.

- `otherOS`: another objectstate instance

The invoking instance becomes a combination of both itself and the passed
`objectstate`. Properties from the invoking instance take precedence over
`otherOS`'s properties.

### Instance Properties
`fn` is a function that is immediately called. All changes that happen during
its execution will be collected into a single `data` event, if the state changed
at all.

#### `os.state`
#### `os.state()`

A reference to the internal state object which represents the state of the
system.
Returns a deep copy of the current state object.

## License

Expand Down
Loading

0 comments on commit 180f07f

Please sign in to comment.