lipp/node-jet

View on GitHub
doc/peer.markdown

Summary

Maintainability
Test Coverage
# jet.Peer API

Load the module "node-jet":

```javascript
var jet = require('node-jet');
```

# `jet.Peer`

The Jet Peer is able to to **consume** content provided by (other) peers:

  - **set** States to new values, see [`peer.set`](#peersetpath-value-options---promise)
  - **call** Methods, see [`peer.call`](#peercallpath-args-options---promise)
  - **fetch** States and Methods as a realtime query, see [`jet.Fetcher`](#jetfetcher)


The Jet Peer is also able to **create** content:

  - add **States** , see [`jet.State`](#jetstate)
  - add **Methods** , see [`jet.Method`](#jetmethod)


## `jet.Peer([config]) -> peer`

Creates and returns a new Jet Peer instance with the specified connection config.
The supported config fields are:

- `url`: {String} The Jet Daemon Websocket URL, e.g. `ws://localhost:11123`
- `ip`: {String} The Jet Daemon TCP trivial protocol ip (default: `localhost`)
- `port`: {String} The Jet Daemon TCP trivial protocol port (default: `11122`)
- `user`: {String, Optional} The user name used for authentication
- `password`: {String, Optional} The password used for authentication
- `rejectUnauthorized`: {Boolean, Optional} Allow self signed server certificates, when using wss:// (default: `false`)
- `headers`: {Object, Optional} Pass additional headers (cookies, bearer etc) to the WebSocket upgrade request

The peer uses either the Websocket protocol or the TCP trivial protocol (default) as transport.
When specifying the `url` field, the peer uses the Websocket protocol as transport.
If no `config` is provided, the Peer connects to the local ('localhost') Daemon using the trivial protocol.
Browsers do only support the Websocket transport and must be provided with a `config` with `url` field.

Authentication is optional and is explained separately.

```javascript
var jet = require('node-jet');

var peer = new jet.Peer({
  url: 'ws://jet.nodejitsu.com:80'
});

peer.connect().then(function() {
  console.log('connection to Daemon established');
  console.log('Daemon Info: ', peer.daemonInfo);
});
```
 
## `peer.connect() -> Promise`

Connects to the Daemon and returns a Promise which gets resolved as the connection is established.
After the connect Promise has been resolved, the peer provides `peer.daemonInfo`.

```javascript
peer.connect().then(function() {
   var daemonInfo = peer.daemonInfo;
   console.log('name', daemonInfo.name); // string
   console.log('version', daemonInfo.version); // string
   console.log('protocolVersion', daemonInfo.protocolVersion); // number
   console.log('can process JSON-RPC batches', daemonInfo.features.batches); // boolean
   console.log('supports authentication', daemonInfo.features.authentication); // boolean
   console.log('fetch-mode', daemonInfo.features.fetch); // string: 'full' or 'simple'
});
```

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.InvalidUser`](#jetinvaliduser)
  - [`jet.InvalidPassword`](#jetinvalidpassword)
 

## `peer.close()`
 
Closes the connection to the Daemon. Returns a promise which resolves as the connection has been closed.

## `peer.isClosed() -> Promise`

Returns a promise which gets resolved when the connection to the Daemon has been closed.

## `peer.set(path, value, [options]) -> Promise`
 
Tries to set the Jet State specified by `path` to `value`. Returns a Promise which gets resolved as
the specified state has been setted successfully to the specified value. 

```javascript
peer.set('foo', 123)
    .then(function() {
        console.log('set finished successfully');
      }).catch(function(err) {
        console.log('set failed', err);
    });

// dont care about the result
peer.set('foo', 12341);
```

`options` is an optional argument, which supports the following fields:

 - `timeout` in seconds. Time to wait before rejecting with a timeout error
 - `valueAsResult` boolean flag. If set `true` the returned Promise get the "real" new value as argument.


```javascript
peer.set('magic', 123, {
  timeout: 7,
  valueAsResult: true
}).then(function(realNewValue) {
    console.log('magic is now', realNewValue);
}).catch(function(err) {
    console.log('set failed', err);
});
```

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.Unauthorized`](#jetunauthorized)
  - [`jet.FetchOnly`](#jetfetchonly)
  - [`jet.NotFound`](#jetnotfound)
  - [`jet.PeerError`](#jetpeererror)
  - [`jet.PeerTimeout`](#jetpeertimeout)
  - [`jet.InvalidArgument`](#jetinvalidargument)


## `peer.call(path, args, [options]) -> Promise`

Calls the Jet Method specified by `path` with `args` as arguments. Returns a Promise which may get resolved with the 
Method call's `result`. An `options` object may be specified which may define a

 - `timeout` in seconds. Time to wait before rejecting with a timeout error


```javascript
peer.call('sum', [1,2,3,4,5]).then(function(result) {
    console.log('sum is', result);
}).catch(function(err) {
    console.log('could not calc the sum', e);
});


// dont care about the result
peer.call('greet', {first: 'John', last: 'Mitchell'});
```
The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.Unauthorized`](#jetunauthorized)
  - [`jet.NotFound`](#jetnotfound)
  - [`jet.PeerError`](#jetpeererror)
  - [`jet.PeerTimeout`](#jetpeertimeout)
  - [`jet.InvalidArgument`](#jetinvalidargument)

## `peer.add(state|method) -> Promise`

Registers a `jet.State` or a `jet.Method` instance at the Daemon. The returned Promise gets resolved
when the Daemon has accepted the request and `set` (States) or `call` (Methods) events may be emitted. 

```javascript
var jim = new jet.State('persons/a4362d', {name: 'jim'});

peer.add(jim).then(function() {
  console.log('jim has been added');
});
```

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.Unauthorized`](#jetunauthorized)
  - [`jet.Occupied`](#jetoccupied)

## `peer.remove(state|method) -> Promise`

Unregisters a `jet.State` or a `jet.Method` instance at the Daemon.
As soon as the returned Promise gets resolved, no `set` or `call` events for the state/method are emitted anymore.

```javascript
var jim = new jet.State('persons/a4362d', {name: 'jim'});

peer.add(jim).then(function() {
  console.log('jim has been added');
  peer.remove(jim).then(function() {
    console.log('jim has been removed');
  });
});

```

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.NotFound`](#jetnotfound)

## `peer.fetch(fetcher) -> Promise`
 
Registers the fetcher at the Daemon. A `jet.Fetcher` must be created first. The returned Promise gets resolved 
when the Daemon has accepted the fetcher and `data` event may be emitted.

```javascript
var topPlayers = new jet.Fetcher()
  .path('startsWith', 'players/')
  .sortByKey('score', 'number')
  .range(1, 10)
  .on('data', function(playersArray) {
  });

peer.fetch(topPlayers);
```

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)

## `peer.unfetch(fetcher) -> Promise`

Unregisters the fetcher at the Daemon. 
As soon as the returned Promise gets resolved, no `data` events for the fetcher are emitted anymore.

# `jet.State`
 
## `new jet.State(path, value, [access])`
 
- `path`: {String} The unique path of the State
- `value`: {Any} The initial value of the State
- `access`: {Object, Optional} Containing `fetchGroups` and `setGroups`

Creates a new State (but does NOT register it at the Daemon).
To register the State call `peer.add`!

```javascript
var john = new jet.State('peoples/25261', {age: 43, name: 'john'}, {
    fetchGroups: ['public'],
    setGroups: ['admin']
});

peer.add(john).then(function() {
});
```

## `state.on('set', cb)`

Registers a `set` event handler. Should be called before the state is actually added (`peer.add(state)`).
The `cb` callback gets the new requested value passed in.

The function is free to:

- return nothing, a State change is posted automatically with the `newValue`
- throw an Error, the Error should be a String or an Object with `code` and `message`
- return on Object with the supported fields:
  - `value`: {Any, Optional} the "real/adjusted" new value. This is posted as the
     new value.
  - `dontNotify`: {Boolean, Optional} Don't auto-send a change Notification


```javascript

john.on('set', function(newValue) {
    var prev = this.value();
    if (newValue.age < prev.age){
      throw 'invalid age';
    }
    return {
        value: {
          age: newValue.age,
          name: newValue.name || prev.name
        }
    };
});

```

To provide an async `set` event handler, provide two arguments to the callback.

- The requested `newValue`
- `reply`: {Function} Method for sending the result/error.

```javascript

john.on('set', function(newValue, reply) {
    setTimeout(function() {
        var prev = this.value();
        if (newValue.age < prev.age){
              reply({
                error: 'invalid age'
            });
        } else {
            reply({
                value: {
                      age: newValue.age,
                      name: newValue.name || prev.name
            }});
        }
    }, 200);
});

```

The arguments to `reply` can be:

  - `value`: {Any} The new value of the state. 
  - `dontNotify`: {Boolean} Dont auto notify a state change.
  - `error`: {String/Error, Optional} Operation failed

## `state.value([newValue])`

If `newValue` is `undefined`, returns the current value. Else posts a value
change Notification that the State's value is now `newValue`.
Use this for spontaneouos changes of a State which were not initially triggered
by the `set` event handler invokation.

```javascript
var ticker = new jet.State('ticker', 1);

peer.add(ticker).then(function() {
  setTimeout(function() {
    var old = ticker.value();
    ticker.value(++old);
  },1000);
});

```

## `state.add() -> Promise`

Register the state from the Daemon convenience function. `peer.add(state)` must have been called before to initially bind the state
to the respective peer!

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.Occupied`](#jetoccupied)

## `state.remove() -> Promise`

Unregister the state from the Daemon. Is the same as calling `peer.remove(state)`.

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.NotFound`](#jetnotfound)
 
# `jet.Fetcher`

## `new jet.Fetcher()`

Creates a new fetcher. All fetcher calls are chainable.
Register the fetcher instance with a call to `peer.fetch(fetcher)`.


## `fetcher.on('data', fetchCb) -> Fetcher`

Installs a callback handler for the `data` event. 
The `fetchCb` argument for non-sorting fetches is an Object with:

- `path`: {String} The path of the State / Method which triggered the Fetch Notification
- `event`: {String} The event which triggered the Fetch Notification ('add', 'remove',
   'change')
- `value`: {Any | undefined} The current value of the State or `undefined` for Methods

```javascript
var movies = new Fetcher()
  .path('startsWith', 'movies')
  .on('data', function(data) {
    console.log(data.path, data.event, data.value);
  });

peer.fetch(movies);
```

For sorting fetch rules, the `fetchCb` arguments are: 

- `sortedStatesArray`: {Array} The sorted states/methods

```javascript
var topTenPlayers = new jet.Fetcher()
  .path('startsWith', 'players/')
  .sortByKey('score', 'number')
  .range(1, 10)
  .on('data', function(topTenPlayersArray) {
  });

peer.fetch(topTenPlayers);
```

## `fetcher.path(predicate, comp) -> Fetcher`

Adds a path matching rule to the fetcher.

[Implemented](https://github.com/lipp/node-jet/blob/master/lib/jet/path_matcher.js#L6) `path` predicates are:

- `startsWith` {String}
- `startsNotWith` {String}
- `endsWith` {String}
- `endsNotWith` {String}
- `contains` {String}
- `containsNot` {String}
- `containsOneOf` {Array of Strings}
- `containsAllOf` {Array of Strings}
- `containsOneOf` {Array of Strings}
- `equals` {String}
- `equalsOneOf` {Array of Strings}
- `equalsNotOneOf` {Array of Strings}

## `fetcher.pathCaseInsensitive() -> Fetcher`

Makes path matching case insensitive.

## `fetcher.expression(expr) -> Fetcher`

Directly sets the fetch expression rule object. This call overwrite all previously set (chained) rules.
```javascript
// The expression may contain the following entries
var expr = {
  path: { // path based matches
    caseInsensitive: true,
    startsWith: 'foo',
    contains: ['bar', 'test']
    // and all other path based match rules
  },
  value: { // value based matches
    equals: 3
    // and other value based match rules
  },
  valueField: {
    'sub.object.path': {
      isType: 'string'
      // and other value/key based match rules
    },
    'another.sub.object.path': {
      lessThan: 5
      // and other value/key based match rules
    }
  },
  sort: {
    asArray: true, // pass items as sorted array to data callback. See fetcher.differential doc
    byPath: true, // either this
    byValue: 'number', // or this (needs supposed js type)
    byValueFiels: { // or this
      'sub.object.path': 'string'
    },
    descending: true,
    from: 1, // starts with index 1 (inclusive range)
    to: 100 // last index 100 (inclusive range)
  }
}
```

## `fetcher.value(predicate, comp) -> Fetcher`

Adds a value matching rule for **primitive type** values to the fetcher.

[Implemented](https://github.com/lipp/node-jet/blob/master/lib/jet/value_matcher.js#L7) predicates are:

- `lessThan` {any less than comparable}
- `greaterThan` {any greater than comparable}
- `equals` {any primitive type}
- `equalsNot` {any primitive type}
- `isType` {String}

## `fetcher.key(keyString, predicate, comp) -> Fetcher`

Adds a key matching rule for **Object type** values to the fetcher. 
Nested keys can be specified like this: `relatives.father.age`.

[Implemented](https://github.com/lipp/node-jet/blob/master/lib/jet/value_matcher.js#L7)  predicates are:

- `lessThan` {any less than comparable}
- `greaterThan` {any greater than comparable}
- `equals` {any primitive type}
- `equalsNot` {any primitive type}
- `isType` {String}

## `fetcher.sortByPath() -> Fetcher`

Adds a sort by path rule to the fetcher.

## `fetcher.sortByValue(type) -> Fetcher`

Adds a sort by value for **primitive types** to the fetcher. Type can be either:

- `number`
- `string`

## `fetcher.sortByKey(keyString, type) -> Fetcher`

Adds a sort by key for **Object types** to the fetcher. Type can be either:

- `number`
- `string`

Nested keys can be specified like this: `relatives.father.age`.

## `fetcher.range(from, to) -> Fetcher`

Adds a sort range to the fetcher. Note that **the first index is 1**. from-to is a closed interval, that
means `fetcher.range(1,10)` gives you up to ten matching states/methods.

## `fetcher.descending() -> Fetcher`

Adds a sort descending rule to the fetcher.

## `fetcher.ascending() -> Fetcher`

Adds a sort ascending rule to the fetcher.

## `fetcher.unfetch() -> Promise`

Unfetches (removes) the Fetcher. `callbacks` is optional.

```javascript
// setup some fetcher
var fetcher = new jet.Fetcher();

fetcher.on('data', function(path, event, value) {
  if (event === 'remove') {
    this.unfetch();
  }
});

peer.fetch(fetcher);
```

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
 
# `jet.Method`

## `new jet.Method(path, [access])`

Creates and returns a Jet Method. To register the Method at the Daemon call `peer.add(method)`.

- `path`: {String} The unique path of the Method
- `access`: {Object, Optional} Containing `fetchGroups` and `callGroups`

```javascript
var greet = new jet.Method('greet', {
    fetchGroups: ['public'],
    callGroups: ['public']
});

peer.add(greet).then(function() {
});
```

## `method.on('call', cb)`

Installs a `call` event handler, which gets executed whenever some peer issues a call request (`peer.call`).

The arguments to the `call` Function are the forwarded "args" field from of original "call" Request.
Either an Array or an Object.

The `call` method can return anything or throw an Error (String/JSON-RPC error)
if required.

```javascript
greet.on('call', function(who) {
    if (who.first === 'John') {
      throw 'John is dismissed';
    }
    var greeting = 'Hello Mr. ' + who.last;
    console.log(greeting);
    return greeting;
});
```

To provide an async `call` event handler, provide two arguments to the callback.

- The forwarded args (Array or Object)
- `reply`: {Function} Method for sending the result/error.


```javascript
greet.on('call', function(who, reply) {
    if (who.first === 'John') {
      throw 'John is dismissed';
    }
    setTimeout(function() {
      var greeting = 'Hello Mr. ' + who.last;
      console.log(greeting);
      reply({
        result: greeting
      });
    }, 100);
});
```


The arguments to `reply` can be:

  - `result`: {Truish, Optional} Operation was success
  - `error`: {String/Error, Optional} Operation failed


## `method.add() -> Promise`

Register the method from the Daemon convenience function. `peer.add(method)` must have been called before to initially bind the method
to the respective peer!

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.Occupied`](#jetoccupied)

## `method.remove() -> Promise`

Unregister the method from the Daemon. Is the same as calling `peer.remove(method)`.

The returned Promise may be rejected with:

  - [`jet.ConnectionClosed`](#jetconnectionclosed)
  - [`jet.NotFound`](#jetnotfound)
 
# Errors

All Peer methods which return a Promise maybe rejected with a typed error. See the doc of each method to see, which
error types this can be for each respective method. For instance the [`peer.add` method](#peeraddstatemethod---promise)
may throw `jet.ConnectionClosed`, `jet.Unauthorized` or `jet.Occupied`.

There are three recommended strategies for handling different errors: 
   - Using `err.name` to distinguish between the types
   - Using `instanceof` operator

```javascript
// by err.name
peer.set('foo', {age: 3, name: 'bar'})
    .then(function() {})
    .catch(function(err) {
      if (err.name === 'NotFound') {
        console.log('foo was not found');
      } else if (err.name === 'PeerTimeout') {
        console.log('foo is not responding fast enough');
      } ...
    });
```

```javascript
// by instanceof
peer.set('foo', {age: 3, name: 'bar'})
    .then(function() {})
    .catch(function(err) {
      if (err instanceof jet.NotFound) {
        console.log('foo was not found');
      } else if (err instanceof jet.PeerTimeout) {
        console.log('foo is not responding fast enough');
      } ...
    });
```

## `jet.BaseError`

Base class for all jet Error types. For all error instances `err instanceof Error` and `err instanceof jet.BaseError` is `true`.
 
## `jet.ConnectionClosed`
 
The connection to the specified endpoint could not be established or has been closed.

## `jet.InvalidUser`

The user (name) provided is not registered at the Daemon.

## `jet.InvalidPassword`

The password provided for the user is not correct.

## `jet.NotFound`

The State or Method specified by `path` has not been added to the Daemon.
One could `fetch` the respective State or Method to wait until it becomes available.

## `jet.Occupied`

A State or Method with the specified `path` can not be added, because another 
State/Method with the same `path` already has been added.

## `jet.PeerTimeout` 

The Peer processing the `set` or `get` request has not answered within the specified timeout.

## `jet.PeerError`

The Peer processing the `set` ot `get` request threw an error during dispatching the request.

## `jet.FetchOnly` 

The State specified by `path` cannot be changed. Some States have strict "monitor" characteristics, as they can be observed 
(by fetched), but not changed by calling `peer.set`.

## `jet.Unauthorized`

The peer instance (user/password) is not authorized to perform the requested action.