stateworks
Version:
Composable stateful objects
150 lines (124 loc) • 5.52 kB
Markdown
# Node Stateworks
*Stateworks* is a library to compose distinct objects into a single
stateful.
*Stateworks* leverages Proxy objects to conduct its magic. Accordingly, the constructor does not return a `Stateful` object, but a `Proxy` around a `StatefulCore` object. The resulting object is more or less a composition of a total of three objects: The `StatefulCore` (which is also an event emitter), the actual state, and the common properties object.
## Update v3
*Stateworks* has been slightly redesigned to use closures instead. Its use is now much like `Promise`s. The `enter` method has effectively been removed from the Stateful object. The `stateful` function no longer takes the initial state as single argument, but an initializer function. See below for details.
# Simple Example
```javascript
const stateful = require('stateworks');
const mystateful = stateful((proxy, common, enter, active) => {
const state1 = {
foo() {
active() === state1; // currently active state, non-proxy. accessing this value circumvents common props
enter(state2);
return 'state1.foo';
}
};
const state2 = {
foo() {
enter(state3);
return 'state2.foo';
}
};
const state3 = {
foo() {
enter({}); // enter empty/terminal state - foo is no longer defined here
return 'state3.foo';
}
};
return state1;
});
console.log(mystateful.foo(), mystateful.foo(), mystateful.foo());
// output: state1.foo state2.foo state3.foo
mystateful.foo(); // throws because 'undefined' is not callable
```
# 2-Layer System
The Stateful object is composed of two objects: the *active state* object and the *common properties*. When transitioning with `enter` the *active state* changes, and thus dynamically the exposed properties along with it.
When getting properties, the *common properties* take precedence. If the property does not exist on the *common properties* object, it is read from the *active state* object. If it doesn't exist there either, `undefined` is returned.
Setting properties follows a very similar logic: if the property exists on the *common properties* object, its property is overridden; otherwise, the *active state*'s property is overridden.
Any method on either the *common properties* or the *active state* are bound to your Stateful object.
**Note** that due to the dynamic nature of these stateful objects, it is impossible to standardize a stateful object's interface for TypeScript as a different state may expose entirely different properties. *Stateworks* is too generic for TypeScript's typing system, though you may define interfaces to describe the current state of the stateful.
## Common Properties
Common properties persist across state transitions. When assigning a value to a Stateful's property, if this property is *common*, it will be stored in the `common` object passed to the initializer. Otherwise, it will be stored in the active state. Thus, when transitioning into another state, this property will be rerouted by the Stateful proxy.
**Example**
```javascript
const stateful = require('stateworks');
const mystateful = stateful((proxy, common, enter) => {
Object.assign(common, {
foo: 'bar',
answer: 42,
})
const state1 = {
state: 1,
ask() {
console.log(this.foo === 'bar'); // true
const answer = this.answer += 1;
enter(state2); // Note that this call already changes the active state
return answer;
}
};
const state2 = {
state: 2,
ask() {
console.log(this.foo === 'bar'); // true
const answer = this.answer += 2
enter(state3);
return answer;
}
};
const state3 = {
state: 3,
ask() {
console.log(this.foo === 'bar'); // true
return this.answer += 1;
}
}
return state1;
});
console.log(mystateful.answer, mystateful.state); // 42 1
console.log(mystateful.ask(), mystateful.state); // 43 2
console.log(mystateful.ask(), mystateful.state); // 45 3
console.log(mystateful.ask(), mystateful.state); // 46 3
```
# Callbacks
If your active state implements `onStateEnter` and `onStateLeave` methods, they will be called accordingly upon transitioning states. `this` will be bound to the Stateful proxy object.
Both callbacks will receive `oldState`, `newState` arguments in this order.
**Example**
```javascript
const stateful = require('stateworks');
const mystateful = stateful((proxy, common, enter) => {
Object.assign(common, {
state: undefined,
});
const state1 = {
onEnterState(oldState, newState) {
newState === state1; // true
this.state = 1;
},
onLeaveState(oldState, newState) {
oldState === state1; // true
newState === state2; // true
console.log('bye bye state 1');
},
next() {
enter(state2);
}
};
const state2 = {
onEnterState() {
this.state = 2;
},
next() {
enter(state1);
}
};
return state1;
});
console.log(mystateful.state); // 1
mystateful.next();
console.log(mystateful.state); // 2
mystateful.next();
console.log(mystateful.state); // 1
```
**Note** that the state returned from the initializer will also trigger `onEnterState`.