diff --git a/docs/API/Realtime.md b/docs/API/Realtime.md new file mode 100644 index 0000000..363ec01 --- /dev/null +++ b/docs/API/Realtime.md @@ -0,0 +1,153 @@ +## Realtime monitoring + +### WebSocket Clients + +We're using [SocketCluster](http://socketcluster.io/) for realtime communication, which provides a fast and scalable webSocket layer (via [`µWS`](https://github.com/uWebSockets/uWebSockets)) and a minimal pub/sub system. You need to include one of [its clients](https://github.com/SocketCluster/client-drivers) in your app to communicate with RemotedevServer. Currently there are clients for [JavaScript (NodeJS)](https://github.com/SocketCluster/socketcluster-client), [Java](https://github.com/sacOO7/socketcluster-client-java), [Python](https://github.com/sacOO7/socketcluster-client-python) and [Objective-C](https://github.com/abpopov/SocketCluster-ios-client). + +By default, the websocket server is running on `ws://localhost:8000/socketcluster/`. + +### Messaging lifecycle + +#### 1. Connecting to the WebSocket server + +The client driver provides a way to connect to the server via websockets (see the docs for the selected client). + +##### JavaScript +```js +var socket = socketCluster.connect({ + hostname: 'localhost', + port: 8000 +}); +``` + +##### Python +```py +socket = Socketcluster.socket("ws://localhost:8000/socketcluster/") +socket.connect() +``` + +#### 2. Disconnecting and reconnecting + +SocketCluster client handles reconnecting for you, but you still might want to know when the connection is established, or when it failed to connect. + +##### JavaScript +```js +socket.on('connect', status => { + // Here will come the next step +}); +socket.on('disconnect', code => { + console.warn('Socket disconnected with code', code); +}); +socket.on('error', error => { + console.warn('Socket error', error); +}); +``` + +##### Python +```py +def onconnect(socket): + // Here will call the next step + +def ondisconnect(socket): + logging.info("on disconnect got called") + +def onConnectError(socket, error): + logging.info("On connect error got called") + +socket.setBasicListener(onconnect, ondisconnect, onConnectError) +``` + +#### 3. Authorizing and subscribing to the channel of events + +We're not providing an authorizing mechanism yet. All you have to do is to emit a `login` event, and you'll get a `channelName` you should subscribe for, and watch for messages and events. Make sure to pass the `master` event, otherwise it should be a monitor, not a client app. + +##### JavaScript +```js +socket.emit('login', 'master', (error, channelName) => { + if (error) { console.log(error); return; } + channel = socket.subscribe(channelName); + channel.watch(handleMessages); + socket.on(channelName, handleMessages); +}); + +function handleMessages(message) { + // 5. Listening for monitor events +} +``` + +##### Python +```py +socket.emitack("login", "master", login) + +def login(key, error, channelName): + socket.subscribe(channelName) + socket.onchannel(channelName, handleMessages) + socket.on(channelName, handleMessages) + +def handleMessages(key, message): + // 5. Listening for monitor events +``` + +You could just emit the `login` event, and omit subscribing (and point `5` bellow) if you want only to log data, not to interact with te app. + +#### 4. Sending the action and state to the monitor + +To send your data to the monitor use `log` or `log-noid` channel. The latter will add the socket id to the message from the server side (useful when the message was sent before the connection was established). + +The message object includes the following: +- `type` - usually should be `ACTION`. If you want to indicate that we're starting a new log (clear all actions emitted before and add `@@INIT`), use `INIT`. In case you have a lifted state similar to one provided by [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument), use `STATE`. +- `action` - the action object. It is recommended to lift it in another object, and add `timestamp` to show when the action was fired off: `{ timestamp: Date.now(), action: { type: 'SOME_ACTION' } }`. +- `payload` - usually the state or lifted state object. +- `name` - name of the instance to be shown in the instances selector. If not provided, it will be equal to `instanceId`. +- `instanceId` - an id to identify the instance. If not provided, it will be the same as `id`. However, it is useful when having several instances (or stores) in the same connection. Also if the user will specify a constant value, it would allow to persist the state on app reload. +- `id` - socket connection id, which should be either `socket.id` or should not provided and use `log-noid` channel. + +##### JavaScript +```js +const message = { + type: 'ACTION', + action: { action, timestamp: Date.now() }, + payload: state, + id: socket.id, + instanceId: window.btoa(location.href), + name: document.title +}; +socket.emit(socket.id ? 'log' : 'log-noid', message); +``` + +##### Python +```py +class Message: + def __init__(self, action, state): + self.type = "ACTION" + self.action = action + self.payload = state + id: socket.id +socket.emit(socket.id if "log" else "log-noid", Message(action, state)); +``` + +#### 5. Listening for monitor events + +When a monitor action is emitted, you'll get an event on the subscribed function. The argument object includes a `type` key, which can be: +- `DISPATCH` - a monitor action dispatched on Redux DevTools monitor, like `{ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE', 'index': 2 }`. See [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js) for details. Additionally to that API, you'll get also a stringified `state` object when needed. So, for example, for time travelling (`JUMP_TO_STATE`) you can just parse and set the state (see the example). Usually implementing this type of actions would be enough. +- `ACTION` - the user requested to dispatch an action remotely like `{ type: 'ACTION', action: '{ type: \'INCREMENT_COUNTER\' }' }`. The `action` can be either a stringified javascript object which should be evalled or a function which arguments should be evalled like [here](https://github.com/zalmoxisus/remotedev-utils/blob/master/src/index.js#L62-L70). +- `START` - a monitor was opened. You could handle this event in order not to do extra tasks when the app is not monitored. +- `STOP` - a monitor was closed. You can take this as no need to send data to the monitor. I there are several monitors and one was closed, all others will send `START` event to acknowledge that we still have to send data. + +See [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js) for an example of implementation without [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js). + +##### JavaScript +```js +function handleMessages(message) { + if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_STATE') { + store.setState(JSON.parse(message.state)); + } +} +``` + +##### Python +```py +def handleMessages(key, message): + if message.type === "DISPATCH" and message.payload.type === "JUMP_TO_STATE": + store.setState(json.dumps(message.state)); +```