bigtestjs / server

All BigTest development has moved to https://github.com/thefrontside/bigtest
https://github.com/thefrontside/bigtest
2 stars 1 forks source link

Add a websocket API to the command server #71

Closed cowboyd closed 4 years ago

cowboyd commented 4 years ago

Closes #58, depends on #72

In order to create reactive UIs whether they are dashboards, CLIs, or even UI running inside the agent, there is the need to receive updates about the state of the server. With our current method of querying the server, it is necessary to fetch the query every time over HTTP that you're interested in the state. However, what we'd really like is for state changes to be sent whenever they are necessary, which can only be known by the server.

This adds a websocket server to the current command server that can send and receive messages in order to issue commands or execute queries via graphql.

The message interface is so:

interface Message {
  responseId?: string;
}

interface QueryMessage extends Message {
  query: string;
  live?: boolean;
}

interface MutationMessage extends Message {
  mutation: string;
}

where the responseId is a purely client generated value that will be sent back transparently with each response so that the client can reconcile itself.

If the live property is passed to a query message, then it will not only run the query and return its result, but it will also listen for state changes and publish that new result to the websocket with the original responseId.

However, unless you're writing a client in a language other than JavaScript, you'll never need to know about responseId at all since you can use the bigtest client. This PR also includes a client API that will eventually be extracted out into @bigtest/client that connects to the web socket command server and lets you issue queries and subscriptions. Here's an example.

import { Client } from './src/client';

function* runQuery(source: string) {
   // this allocates the client and connects the socket
   // returning only once the socket is connected and open
   // The underlying socket will be closed once this context is exited.
   // any errors in the socket will cause this context to fail.
   let client: Client = yield Client.create('ws://localhost:24002');

   // do a one shot query.
   let { data } = yield client.query('{ agents { browser { name } } }');

   // subscribe to a query by passing an operation run for each published result
  yield client.subscribe('{ manifest { path } }', function*(data) {
    console.log('manifest updated', data);
  });
}

I was going to hold off on writing a client, but it turned out to be necessary to write the tests.

Finally, I added two little scripts bin/query.ts and bin/subscribe.ts to be little utilities to run commands against the server:

query state:

$ yarn ts-node ./bin/query.ts "{ agents { browser { name } } }"
{
  "agents": [
    {
      "browser": {
        "name": "Safari"
      }
    }
  ]
}

Or subscribe to a query

$ yarn ts-node ./bin/subscribe.ts "{ agents { browser { name } } }"
==== new subscription result ====
{
  "agents": [
    {
      "browser": {
        "name": "Safari"
      }
    }
  ]
}
==== new subscription result ====
{
  "agents": [
    {
      "browser": {
        "name": "Safari"
      }
    },
    {
      "browser": {
        "name": "Chrome"
      }
    }
  ]
}
==== new subscription result ====
{
  "agents": [
    {
      "browser": {
        "name": "Safari"
      }
    },
    {
      "browser": {
        "name": "Chrome"
      }
    },
    {
      "browser": {
        "name": "Firefox"
      }
    }
  ]
}

TODOS and Open Questions