npm package: @ircam/shared-state
Description: > Library dedicated at maintaining state(s) synchronized accross nodes  > of a network.  > The library doesn't implement any transport method, however it should >  work out of the box with popular socket libraries that implement an > `EventEmitter` API (e
Latest version: 0.0.0-alpha.0

# `shared-state`

> Library dedicated at maintaining state(s) synchronized accross nodes 
> of a network. 
> The library doesn't implement any transport method, however it should
>  work out of the box with popular socket libraries that implement an
> `EventEmitter` API (e.g. `socket.io`, or a simple `EventEmitter` interface  
> on top of `WebSocket`)

## Installation

```sh
npm install @ircam/shared-state --save
```

## Usage

```js
import { Server } from '@ircam/shared-state';

const server = new Server();

server.registerSchema('my-schema', {
  myInteger: {
    type: 'integer',
    default: 0,
  },
  // ....
});

// `clientId` - unique id of a client
// `transport` - some transport (WebSocket, EventEmitter) implementing a basic
// event API (i.e. `emit`, `addListener`, `removeAllListeners`)
server.addClient(clientId, transport);

const state = await server.create(`my-schema`);
await state.set({ myInterger: 2 })

state.getValues();
// > { myInteger: 2 }
```

```js
import { Client } from '@ircam/shared-state';

const client = new Client(nodeId, transport);

const state = await client.attach('my-schema');
state.subscribe(updates => {
  // > updates = { myInteger: 2 }
});

```

## API

### `StateManager` API 

#### stateManager.create(schemaName, synced = true) => Promise(State)

Create a synced `my-state` state where `owner` is the server and `creator` is the client

```
const state = await stateManager.create('my-schema');
```

Create a local `my-state` state where `owner` and `creator` are owner 
@important - the state is purely local then no other node can attach to it

```
const state = await stateManager.create('my-schema', false);
```

#### stateManager.attach(schemaName, nodeId = SERVER_ID) => Promise(State) 

Attach to a state created by another client

```
const state = await stateManager.attach('my-schema', clientId);
```

_client-side only_: attach to a state created by the server

```
const state = await stateManager.attach('my-schema');
```

#### stateManager.observe(listener: Function) => void

Observe the creation of new states, the callback is called a first time with a list of all already created states, and then each time a new state is created.

```
// @note - unroll first set in the stateManager itself
stateManager.observe((schemaName, nodeId) => {
    // the node is interested in 'whatever' schemaName
    if (schemaName === 'whatever') {
      const state = await stateManager.attach(schemaName, nodeId);
      // do things with `state`
    }
  }
});
```

## Client Only API

### `StateManager` API

#### StateManager.constructor(client);

### Server Only API

@note: by convention the server `nodeId` is `-1`

#### StateManager.constructor();

#### stateManager.registerSchema(schemaName, definitions);

#### stateManager.addClient(nodeId, transport);

#### stateManager.removeClient(nodeId)

### `State` API

A state cannot be instanciated manually, its always created using the `StateManager` factory methods `create` or `attach`.

#### state.getSchema() => Object

Returns the schema of the state as registered in the server.

```
const schema = state.getSchema();
```

#### async state.set(updates: Object) => Promise(newValues)

Update state with new values. Given values may be ignored or sanitized according to the definition of the parameter.

```
// given the schema
//  {
//    myInt: {
//      type: 'integer',
//      min: 0,
//      max: 10,
//    }
//  }
const updated = await state.set({ myInt: -4 })
// updated.myInt === 0
```

#### state.get(name: String) => Mixed

Return the current value of the `name` state entry.

#### state.getValues() => Object

Return the current values of the state as flat <key, value> object

#### state.subscribe(listener: Function) => Function

Subscribe to modifications of the state. Return a function that allow to unsubscribe the listener.

```
const unsubscribe = state.subscribe(updates => {
  for (let key in updates) {
    switch (key) {
      // ...
    }
  }

  doSomethingWithAll(state.getValues());
});

// ...later
unsubscribe();
```

#### state.detach() => Promise

Detach from the state:
- If the node is the creator of the state, then the state is destroyed and all other attached nodes will be detached. 
- If the node is not the creator of the state, the node stops receiving new updates about the state.

Calling detach immediatly removes all listeners subscribed to the state, then requests the server (owner of the state) for detaching. Calls the `onDetach` listeners when acknoledgement is received from the server.

```
state.detach();
```

#### state.onDetach(callback: Function) => void

```
state.onDetach(() => {
  // clean everything related to the state
});
```


## License

BSD-3-Clause