redux-devtools/docs/Integrations/Remote.md
2021-03-06 13:56:00 -05:00

7.8 KiB

Remote monitoring

By installing @redux-devtools/cli, starting the server server and launching the Redux DevTools app (redux-devtools --open), you can connect any remote application, even not javascript. There are some integrations for javascript like remote-redux-devtools and remotedev, but the plan is to deprecate them and support it out of the box from the extension without a websocket server. It is more useful for non-js apps.

WebSocket Clients

We're using SocketCluster for realtime communication, which provides a fast and scalable webSocket layer and a minimal pub/sub system. You need to include one of its clients in your app to communicate with RemotedevServer. Currently there are clients for JavaScript (NodeJS), Java, Python, C, Objective-C and .NET/C#.

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
var socket = socketCluster.connect({
  hostname: 'localhost',
  port: 8000,
});
Python
socket = Socketcluster.socket("ws://localhost:8000/socketcluster/")
socket.connect()

Note that JavaScript client composes the url from hostname and port, adding /socketcluster/ path automatically. For other clients, you should specify that path. For example, for ObjectiveC it would be self.client.initWithHost("localhost/socketcluster/", onPort: 8000, securely: false).

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
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
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
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
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, 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
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
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 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.
  • 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 for an example of implementation without redux-devtools-instrument.

JavaScript
function handleMessages(message) {
  if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_STATE') {
    store.setState(JSON.parse(message.state));
  }
}
Python
def handleMessages(key, message):
  if message.type === "DISPATCH" and message.payload.type === "JUMP_TO_STATE":
    store.setState(json.loads(message.state));