-
Notifications
You must be signed in to change notification settings - Fork 19
Feature: Move order into a placed
state
#66
Changes from all commits
24dc178
7ddc93c
1a950ed
0b202a2
27eba3a
e26c3a5
037360b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,7 @@ enum OrderStatus { | |
FILLED = 2; | ||
EXECUTED = 3; | ||
COMPLETED = 4; | ||
REJECTED = 5; | ||
} | ||
|
||
message Order { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const OrderStateMachine = require('./order-state-machine') | ||
|
||
module.exports = { | ||
OrderStateMachine | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,16 @@ | ||
const { promisify } = require('util') | ||
const { getRecords } = require('../utils') | ||
const StateMachine = require('javascript-state-machine') | ||
const StateMachineHistory = require('javascript-state-machine/lib/history') | ||
const { Order } = require('../models') | ||
|
||
/** | ||
* @class Finite State Machine for managing order lifecycle | ||
*/ | ||
const OrderStateMachine = StateMachine.factory({ | ||
plugins: [ | ||
new StateMachineHistory() | ||
], | ||
/** | ||
* Definition of the transitions and states for the OrderStateMachine | ||
* @type {Array} | ||
|
@@ -17,11 +21,21 @@ const OrderStateMachine = StateMachine.factory({ | |
* @type {Object} | ||
*/ | ||
{ name: 'create', from: 'none', to: 'created' }, | ||
/** | ||
* place transition: second transition in the order lifecycle | ||
* @type {Object} | ||
*/ | ||
{ name: 'place', from: 'created', to: 'placed' }, | ||
/** | ||
* goto transition: go to the named state from any state, used for re-hydrating from disk | ||
* @type {Object} | ||
*/ | ||
{ name: 'goto', from: '*', to: (s) => s } | ||
{ name: 'goto', from: '*', to: (s) => s }, | ||
/** | ||
* reject transition: a created order was rejected during placement | ||
* @type {Object} | ||
*/ | ||
{ name: 'reject', from: 'created', to: 'rejected' } | ||
], | ||
/** | ||
* Instantiate the data on the state machine | ||
|
@@ -38,6 +52,67 @@ const OrderStateMachine = StateMachine.factory({ | |
return { store, logger, relayer, engine, order: {} } | ||
}, | ||
methods: { | ||
/** | ||
* Wrapper for running the next transition with error handling | ||
* @param {string} transitionName Name of the transition to run | ||
* @param {...Array} arguments Arguments to the apply to the transition | ||
* @return {void} | ||
*/ | ||
nextTransition: function (transitionName, ...args) { | ||
this.logger.debug(`Queuing transition: ${transitionName}`) | ||
process.nextTick(async () => { | ||
this.logger.debug(`Running transition: ${transitionName}`) | ||
try { | ||
if (!this.transitions().includes(transitionName)) { | ||
throw new Error(`${transitionName} is invalid transition from ${this.state}`) | ||
} | ||
|
||
await this[transitionName](...args) | ||
} catch (e) { | ||
// TODO: bubble/handle error | ||
// TODO: rejected state to clean up paid invoices, etc | ||
this.logger.error(`Error encountered while running ${transitionName} transition`, e) | ||
this.reject(e) | ||
} | ||
}) | ||
}, | ||
|
||
/** | ||
* Save the current state of the state machine to the store using the `host` as a carrier | ||
* @param {Object} host Host object to store in the data store with state machine metadata attached | ||
* @return {Promise<void>} Promise that resolves when the state is persisted | ||
*/ | ||
persist: async function ({ key, valueObject }) { | ||
if (!key) { | ||
throw new Error(`An order key is required to save state`) | ||
} | ||
|
||
if (!valueObject) { | ||
// console.log('this.order', this.order) | ||
throw new Error(`An Order object is required to save state`) | ||
} | ||
|
||
const { state, history } = this | ||
let error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] would change this to |
||
|
||
if (this.error) { | ||
error = this.error.message | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when would an error not have a message in our code? Do our custom errors not implement this correctly? |
||
|
||
if (!error) { | ||
this.logger.error('Saving state machine error state with no error message', { key }) | ||
} | ||
} | ||
|
||
const stateMachine = { state, history, error } | ||
|
||
const value = JSON.stringify(Object.assign(valueObject, { __stateMachine: stateMachine })) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if we need to for this PR, but would be worth it to throw this into its own serialize/deserialize method, in the same way we have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was part of the desire for #67 |
||
|
||
// somehow spit an error if this fails? | ||
await promisify(this.store.put)(key, value) | ||
|
||
this.logger.debug('Saved state machine in store', { orderId: this.order.orderId }) | ||
}, | ||
|
||
onBeforeTransition: function (lifecycle) { | ||
this.logger.info(`BEFORE: ${lifecycle.transition}`) | ||
}, | ||
|
@@ -55,18 +130,11 @@ const OrderStateMachine = StateMachine.factory({ | |
|
||
if (lifecycle.transition === 'goto') { | ||
this.logger.debug('Skipping database save since we are using a goto') | ||
return | ||
} | ||
|
||
if (lifecycle.to === 'none') { | ||
} else if (lifecycle.to === 'none') { | ||
this.logger.debug('Skipping database save for the \'none\' state') | ||
return | ||
} else { | ||
this.persist(this.order) | ||
} | ||
|
||
// somehow spit an error if this fails? | ||
await promisify(this.store.put)(this.order.key, JSON.stringify(Object.assign(this.order.valueObject, { __state: this.state }))) | ||
|
||
this.logger.debug('Saved state machine in store', { orderId: this.order.orderId }) | ||
}, | ||
onAfterTransition: function (lifecycle) { | ||
this.logger.info(`AFTER: ${lifecycle.transition}`) | ||
|
@@ -100,6 +168,40 @@ const OrderStateMachine = StateMachine.factory({ | |
this.order.addCreatedParams(await this.relayer.createOrder(this.order.createParams)) | ||
|
||
this.logger.info(`Created order ${this.order.orderId} on the relayer`) | ||
}, | ||
|
||
/** | ||
* Attempt to place the order as soon as its created | ||
* @param {Object} lifecycle Lifecycle object passed by javascript-state-machine | ||
* @return {void} | ||
*/ | ||
onAfterCreate: function (lifecycle) { | ||
this.logger.info(`Create transition completed, triggering place`) | ||
|
||
this.nextTransition('place') | ||
}, | ||
|
||
/** | ||
* Place the order on the relayer during transition. | ||
* This function gets called before the `place` transition (triggered by a call to `place`) | ||
* Actual placement on the relayer is done in `onBeforePlace` so that the transition can be cancelled | ||
* if placement on the Relayer fails. | ||
* | ||
* @param {Object} lifecycle Lifecycle object passed by javascript-state-machine | ||
* @return {Promise} Promise that rejects if placement on the relayer fails | ||
*/ | ||
onBeforePlace: async function (lifecycle) { | ||
throw new Error('Placing orders is currently un-implemented') | ||
}, | ||
|
||
/** | ||
* Handle rejection by assigning the error to the state machine | ||
* @param {Object} lifecycle Lifecycle object passed by javascript-state-machine | ||
* @param {Object} error Error that triggered rejection | ||
* @return {void} | ||
*/ | ||
onBeforeReject: function (lifecycle, error) { | ||
this.error = error | ||
} | ||
} | ||
}) | ||
|
@@ -136,12 +238,27 @@ OrderStateMachine.getAll = async function ({ store, ...initParams }) { | |
*/ | ||
OrderStateMachine.fromStore = function (initParams, { key, value }) { | ||
const parsedValue = JSON.parse(value) | ||
const stateMachine = parsedValue.__stateMachine | ||
|
||
if (!stateMachine) { | ||
throw new Error('Values must have a `__stateMachine` property to be created as state machines') | ||
} | ||
|
||
const orderStateMachine = new OrderStateMachine(initParams) | ||
|
||
orderStateMachine.order = Order.fromObject(key, parsedValue) | ||
|
||
orderStateMachine.goto(parsedValue.__state) | ||
// re-inflate state machine properties | ||
|
||
// state machine current state | ||
orderStateMachine.goto(stateMachine.state) | ||
|
||
// state machine history | ||
orderStateMachine.clearHistory() | ||
orderStateMachine.history = stateMachine.history | ||
|
||
// state machine errors | ||
orderStateMachine.error = new Error(stateMachine.error) | ||
|
||
return orderStateMachine | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what was the reasoning for using
nextTick
here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't move to another transition while you're still in one.
nextTick
gets you out of the current transition. See this issue: jakesgordon/javascript-state-machine#143