bigtestjs / server

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

Refactor State object into a reactive atom #72

Closed cowboyd closed 4 years ago

cowboyd commented 4 years ago

In order to make reactivity of the bigtest server state a snap, we need a way to receive notifications about state changes.

This refactors the current state application into a "reactive atom" that is accessed via operations instead of synchronous methods. I tried this approach because it's definitely anticipated that we are going to be reacting to this atom at a bunch of different points throughout the orchestrator tree, and part of those reactions is that you will need to execute operations within a specific effection context in order for code not to "leak" into an unsafe space where it could block forever or throw an exception that isn't handled.

We could just make the state object an event emitter, but then, in order to use it to handle an event from anywhere, you have to keep a reference to the context where you plan on using it, so that you can "re-enter" it by spawning the event handler at that point in the tree. That's got some complexity because now in order to be technically safe, you need to make sure and validate the context is still running before dispatching the event to the handler.

The other problem with a synchronous style callback like an EventEmitter, is that if any of the event listeners throw an exception, then that will percolate inside the emitting stack, and not the handling stack. This almost certainly never what you want.

By making the atom "operation native" it can be seamlessly composed into any other operation, either with the generator API, but also using the simple function API. It's easy to imagine adding some handy operations to atom.

yield atom.forEach(function*(state) {
  console.log(`new state`, state);
});
yield atom.whenever(state => state.agents.length > 10, function*() {
  console.log('did you mean to connect more than 10 agents?');
});

Or if we ended up adding some more functional composition things that worked over all Operations

function map(operation: Operation<A>, fn: a => b): Operation<B> {
  return fn(yield operation);
}

function* useAgents() {
  let agentArray = yield map(agent.next(), state => Object.values(state.agents));
}