Last active
March 10, 2020 18:15
-
-
Save ggoodman/d26ee53823b0a6ce42c178dd9c89cb18 to your computer and use it in GitHub Desktop.
Definition for a state machine that tracks a node http request through its lifecycle
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"PendingSocket": { | |
"kind": "Intermediate", | |
"onEnter": [ | |
{} | |
], | |
"onEvent": { | |
"Error": [ | |
{ | |
"targetStates": [ | |
"Error" | |
] | |
} | |
], | |
"Socket": [ | |
{ | |
"targetStates": [ | |
"SocketObtained" | |
] | |
} | |
] | |
}, | |
"onExit": [ | |
{} | |
] | |
}, | |
"SocketObtained": { | |
"kind": "Intermediate", | |
"onEnter": [ | |
{ | |
"targetStates": [ | |
"SocketConnected" | |
] | |
}, | |
{} | |
], | |
"onEvent": { | |
"Error": [ | |
{ | |
"targetStates": [ | |
"Error" | |
] | |
} | |
], | |
"Connect": [ | |
{ | |
"targetStates": [ | |
"SocketConnected" | |
] | |
} | |
] | |
}, | |
"onExit": [ | |
{} | |
] | |
}, | |
"SocketConnected": { | |
"kind": "Intermediate", | |
"onEnter": [ | |
{} | |
], | |
"onEvent": { | |
"Error": [ | |
{ | |
"targetStates": [ | |
"Error" | |
] | |
} | |
], | |
"Response": [ | |
{ | |
"targetStates": [ | |
"Responded" | |
] | |
} | |
] | |
}, | |
"onExit": [ | |
{} | |
] | |
}, | |
"Responded": { | |
"kind": "Final", | |
"onEnter": [], | |
"onEvent": {}, | |
"onExit": [] | |
}, | |
"Error": { | |
"kind": "Final", | |
"onEnter": [], | |
"onEvent": {}, | |
"onExit": [] | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export enum RequestEventKind { | |
Error = 'Error', | |
Socket = 'Socket', | |
Connect = 'Connect', | |
Response = 'Response', | |
} | |
export type ErrorEvent = { | |
eventName: RequestEventKind.Error; | |
err: NodeJS.ErrnoException; | |
}; | |
export type SocketEvent = { | |
eventName: RequestEventKind.Socket; | |
socket: Net.Socket; | |
}; | |
export type ConnectEvent = { | |
eventName: RequestEventKind.Connect; | |
}; | |
export type ResponseEvent = { | |
eventName: RequestEventKind.Response; | |
res: Http.IncomingMessage; | |
}; | |
export type RequestEvent = ErrorEvent | SocketEvent | ConnectEvent | ResponseEvent; | |
export enum RequestStateKind { | |
PendingSocket = 'PendingSocket', | |
SocketObtained = 'SocketObtained', | |
SocketConnected = 'SocketConnected', | |
Responded = 'Responded', | |
Error = 'Error', | |
} | |
export type PendingSocketState = FSM.IntermediateState<{ | |
stateName: RequestStateKind.PendingSocket; | |
disposer: DisposableStore; | |
req: Http.ClientRequest; | |
}>; | |
export type SocketObtainedState = FSM.IntermediateState<{ | |
stateName: RequestStateKind.SocketObtained; | |
disposer: DisposableStore; | |
req: Http.ClientRequest; | |
socket: Net.Socket; | |
}>; | |
export type SocketConnectedState = FSM.IntermediateState<{ | |
stateName: RequestStateKind.SocketConnected; | |
disposer: DisposableStore; | |
req: Http.ClientRequest; | |
}>; | |
export type RespondedState = FSM.FinalState<{ | |
stateName: RequestStateKind.Responded; | |
req: Http.ClientRequest; | |
res: Http.IncomingMessage; | |
}>; | |
export type ErrorState = FSM.FinalState<{ | |
stateName: RequestStateKind.Error; | |
}>; | |
export type RequestState = | |
| PendingSocketState | |
| SocketObtainedState | |
| SocketConnectedState | |
| RespondedState | |
| ErrorState; | |
const machineTemplate = createMachineFactory<RequestEvent, RequestState>() | |
.state(RequestStateKind.PendingSocket, builder => | |
builder | |
.onEnterExecute((state, _event, send) => { | |
state.disposer.add( | |
Event.fromNodeEventEmitter<NodeJS.ErrnoException>( | |
state.req, | |
'error' | |
)(err => send({ eventName: RequestEventKind.Error, err })) | |
); | |
state.disposer.add( | |
Event.fromNodeEventEmitter<Net.Socket>( | |
state.req, | |
'socket' | |
)(socket => send({ eventName: RequestEventKind.Socket, socket })) | |
); | |
}) | |
.onEventTransition( | |
RequestEventKind.Error, | |
(_state, event) => { | |
return { | |
stateName: RequestStateKind.Error, | |
err: event.err, | |
}; | |
}, | |
{ targetStates: [RequestStateKind.Error] } | |
) | |
.onEventTransition( | |
RequestEventKind.Socket, | |
(state, event) => { | |
return { | |
stateName: RequestStateKind.SocketObtained, | |
disposer: new DisposableStore(), | |
req: state.req, | |
socket: event.socket, | |
}; | |
}, | |
{ targetStates: [RequestStateKind.SocketObtained] } | |
) | |
.onExitExecute(state => { | |
state.disposer.dispose(); | |
}) | |
) | |
.state(RequestStateKind.SocketObtained, builder => | |
builder | |
// If the socket isn't connecting, we transition straight to | |
// the SocketConnected state. | |
.onEnterTransition( | |
state => { | |
return { | |
stateName: RequestStateKind.SocketConnected, | |
disposer: new DisposableStore(), | |
req: state.req, | |
}; | |
}, | |
{ | |
condition: state => !state.socket.connecting, | |
targetStates: [RequestStateKind.SocketConnected], | |
} | |
) | |
// | |
.onEnterExecute((state, _event, send) => { | |
state.disposer.add( | |
Event.fromNodeEventEmitter<NodeJS.ErrnoException>( | |
state.socket, | |
'error' | |
)(err => send({ eventName: RequestEventKind.Error, err })) | |
); | |
state.disposer.add( | |
Event.fromNodeEventEmitter<Net.Socket>( | |
state.socket, | |
'connect' | |
)(() => send({ eventName: RequestEventKind.Connect })) | |
); | |
}) | |
.onEventTransition( | |
RequestEventKind.Error, | |
(_state, event) => { | |
return { | |
stateName: RequestStateKind.Error, | |
err: event.err, | |
}; | |
}, | |
{ targetStates: [RequestStateKind.Error] } | |
) | |
.onEventTransition( | |
RequestEventKind.Connect, | |
state => { | |
return { | |
stateName: RequestStateKind.SocketConnected, | |
disposer: new DisposableStore(), | |
req: state.req, | |
}; | |
}, | |
{ targetStates: [RequestStateKind.SocketConnected] } | |
) | |
.onExitExecute(state => { | |
state.disposer.dispose(); | |
}) | |
) | |
.state(RequestStateKind.SocketConnected, builder => | |
builder | |
.onEnterExecute((state, _event, send) => { | |
state.disposer.add( | |
Event.fromNodeEventEmitter<NodeJS.ErrnoException>( | |
state.req, | |
'error' | |
)(err => send({ eventName: RequestEventKind.Error, err })) | |
); | |
state.disposer.add( | |
Event.fromNodeEventEmitter<Http.IncomingMessage>( | |
state.req, | |
'response' | |
)(res => send({ eventName: RequestEventKind.Response, res })) | |
); | |
}) | |
.onEventTransition( | |
RequestEventKind.Error, | |
(_state, event) => { | |
return { | |
stateName: RequestStateKind.Error, | |
err: event.err, | |
}; | |
}, | |
{ targetStates: [RequestStateKind.Error] } | |
) | |
.onEventTransition( | |
RequestEventKind.Response, | |
(state, event) => { | |
return { | |
stateName: RequestStateKind.Responded, | |
req: state.req, | |
res: event.res, | |
}; | |
}, | |
{ targetStates: [RequestStateKind.Responded] } | |
) | |
.onExitExecute(state => { | |
state.disposer.dispose(); | |
}) | |
) | |
.finalState(RequestStateKind.Responded) | |
.finalState(RequestStateKind.Error); | |
// This produces the content of definition.json | |
console.log(JSON.stringify(machineTemplate, null, 2)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment