# JavaScript Messenger client

The messenger client provides JavaScript classes aimed as subscribing and publishing to nchan channels. Implemented features:
 * listening to messages on a list of channels with auto-reconnecting on lost connection
 * subscribing to messages from a subset of channels
 * publishing send-and-forget messages on any channel
 * sending requests to channels and waiting for a response with optional progress notifications

## `Messenger` class overview

The `Messenger` class provides an interface for several clients to a single medium (currently: a WebSocket connection) and to publish messages to arbitrary channels. A client can subscribe to a messenger and publish messages before a connection is established: the messages will be sent as soon as messenger connects to nchan.

*Warning: with current implementation no requests should be made when no connection exists.*

### `UrlFactory` class

The messenger class must be aware of how to translate a collection of publisher and subscriber channels to an nchan location. This is implemented with the class `UrlFactory` (or its subclass `ControllerUrlFactory`) that is provided to the constructor.
```javascript
import { UrlFactory } from './medium/urlfactory.js';

const urlFactory = new UrlFactory('./nchan-channels/', 'mygroup', {
    sub: ['ch1', 'ch2'],
    pub: ['ch3', 'ch4']
});
const msgr = new Messenger(urlFactory, {
    pingInterval: 30, // seconds
    autoReconnect: true
});
```
The constructor of `UrlFactory` takes the following parameters:
 1. `baseUrl`: the base url for the messenger service
 2. `group`: the group of channels
 3. `channels`: an object with `pub` and `sub` properties (optional)

The class provides the following methods used by `Messenger` class:
* `restEndpoint(path)`: returns the URL for a REST service endpoint
* `publisherEndpoint(channels)`: returns the URL for publishing to the given channels
* `messengerEndpoint(token)`: returns the WebSocket URL for the messenger

#### ControllerUrlFactory

A specialized factory for administrative connections that uses a different URL structure for the messenger endpoint. Only `sub` channels are used.

### Establishing a connection

A connection is opened and closed with async methods `open()` and `close()`. Each method resolves on success and rejects on a failure. It is safe to call them more than once. Once a connection is opened or closed, the object dispatched an event (`open` or `close` respectively). Listeners to those events can be assigned with `addEventListener()` method or by assigning a callback to the property `onopen` or `onclose`. There are also async methods `connected()`, `opened()` and `closed()` that resolve when the desired state is reached, but they do not reject otherwise.

```javascript
// Listening with events: these functions will be called each time
messenger.onopen = () => console.log(`Connected to ${messenger.url}`);
messenger.onclose = () => console.log(`Disconnected from ${messenger.url}`);

// React when the low-level connection has been established
messenger.connected().then(() => console.log("Configuring the connection..."));

// Wait until the messenger receives meta data of the channel for the server
messenger.opened().then(() => console.log("I told you I'll connect eventually!"));

// Connect and do something afterwards
messenger.open().then(
    () => console.log("I'm opened!"),
    err => console.error('Connection has failed:', err)
);
```
The messenger will attempt to reconnect when a connection is lost or not established. Thence, `open()` does not reject on the first failure, but
* when `close()` is called
* when the connection is lost with a code indicating that further attempts will also fail.


### Connection state

The property `state` is the current connection state of the messenger. It takes one of the following values:

| Constant              | Remarks                                                           |
|-----------------------|-------------------------------------------------------------------|
|`Messenger.WAITING`    | An attempt to reconnect will be made after closed prematurely.    |
|`Messenger.CONNECTING` | An instance of WebSocket class is created, but not connected yet. |
|`Messenger.CONFIGURING`| The messenger waits for the initial `hello` message.              |
|`Messenger.CONNECTED`  | Fully functional connection.                                      |
|`Messenger.CLOSING`    | The connection is being closed. This stage is usually very short. |
|`Messenger.CLOSED`     | No instance of WebSocket class exists.                            |

The property `desiredState` is the state, which the class tries to reach. Can be one of `Messenger.CONNECTED` or `Messenger.CLOSED`.

Each time the value of `state` is changed, an event `statechange` is dispatched:
```javascript
// or messenger.onstatechange = ...
messenger.addEventListener('statechange', evt => {
    // evt is of type ChangeEvent that extends CustomEvent and provides
    // special properties for the previous and current value
    console.log(`state change: ${evt.previousValue} -> ${evt.currentValue}`);
});
```
One can also wait for `state` to reach a desired value with an async method `waitForConnectionState(awaitedState)`.


### Digression - aborting async methods

Most async methods take an object `promiseOptions` parameter with optional properties
* `timeout`: the number of miliseconds after which the method rejects with `TimeoutError`
* `signal`: an `AbortSignal` that cancels the promise and causes the method to reject with `AbortError`.

```javascript
const openCtl = new AbortController();
waitTillConnectionNoLongerNeeded()
    .then(() => openCtl.abort('no longer needed'));
...
try {
    await messenger.opened({
        timeout: 5000,              // wait only 5s then stop
        signal: openCtl.signal     // 
    })
} catch (error) {
    if (error.name === 'TimeoutError') {
        console.error('No connection after 5sec!');
    } else if (error.name === 'AbortError') {
        console.error(error.reason);
    }
}
```
Signals can be also passed to event listeners - the listener is removed from the list when the abort signal is received.
```javascript
// The log is not printed if openCtl.abort() is called before the event
// is fired
messenger.addEventListener('open', () => console.log('Opened!'), {
    signal: openCtl.signal
})
```

### Access Tokens

Access to channels is protected by tokens. Multiple tokens can be added to a messenger instance.

```javascript
// Add any number of tokens to the messenger
messenger.tokens.add('first-token', 'second-token', 'third-token');

// The messenger can merge multiple tokens into a single one
// by calling the server-side REST API. This is done internally
// on all provided tokens before a connection is established.
const mergedToken = await messenger.tokens.merged();
```

### Subscribing to channels

A client can be registered to receive messages from a subset of channels by provided two methods
* `handleMessage(msg)` to handle notifications (messages with no response assumed)
* `handleRequest(request)` to handle requests (messages that wait for a response).

```javascript
// Create a handler for a notification bar
//
// Notifications are received on the channel "notification".
// We assume that the payload is an object with properties
// - content: the text to be displayed
// - style: the CSS class name to be set
// This element does not support requests.
const notificationBarHandler = {
    container: document.querySelector('#notification-bar-content'),
    handleMessage({
        timestamp,  // timestamp of the message in seconds
        name,       // name of the message - ignored here
        channel,    // the name of the channel of the message
        payload     // message payload
    }) {
        const { content='', style='' } = payload;
        // Date requires timestamp in miliseconds
        const date = (new Date(timestamp*1000)).toLocaleTime();
        this.container.className = style;
        this.container.textContent = content + ` (updated on ${data})`;
    }
}
// One can omit the second parameter to listen to all channels
messenger.subscribe(notificationBarHandler, [ 'notification', 'urgent' ]);

// Unsubscribe when no longer needed. Can be done partially.
messenger.unsubscribe(notificationBarHandler, [ 'urgent' ]);
```

### Publishing messages

A message can be published on any channel with `publish()` method. The underlying WebSocket connection is used if no channels are specified or if the list of channels match the publisher channels of the websocket. Otherwise an HTTP request is made to the appropriate Nchan location.

```javascript
messenger.publish(
    // message name
    'updated',     
    // message payload: json serializable value (object literal, array, string)
    { id: 723, status: 53 },
    // a list of channels
    [ 'order' ]
);
// Optionally wait for messages to be processed and sent
// This is not quite reliable though - this method resolves as soon as messages
// are passed to the underlying WebSocket object. Hence, they are still in the
// internal buffer.
await messenger.drain();
```

### Making requests

A request is a message that expects a response. While preparing a response, the requester can send progress notifications. This process is controlled with classes `Request` and `Response`.

```javascript
/** Client A: makes a request and reacts to progress notifications **/

// futureResponse is of type Response from js/medium/protocol.js
const futureResponse = await msgrA.request(
    // request name
    'server-status',
    // request payload
    { type: 'compute', data: 'load,memory,processes,users' }
    // optionally: channels (default: all), timeout, abort signal
    { channels: ['monitor'], timeout: 10000 }
);

// React to progress notifications
futureResponse.onprogress = evt => {
    // evt is of type MessageEvent with evt.data the notification payload
    console.log(`Excuse given: ${evt.data}`);
}

// Wait for the final response
const data = await futureResponse.payload();

/** Client B: processes the request **/

msgrB.subscribe({
    // a handler can be an async method
    // request is of type Request from js/medium/protocol.js
    async handleRequest(request) {
        // Send a notification five times every second
        for (let i=5; i>0; i--) {
            await new Promise(res => setTimeout(res, 1000));
            request.notify(`give me ${i} more seconds`);
        }
        // Send a response - can be any json serializable data
        request.respond({
            load: 123,
            memory: 478123,
            processes: 1432,
            users: 12
        }); 
    }
}, ['monitor']);
```
**Note**: the messenger class takes care of not calling request handlers multiple times if the same request appears on several channels.
