To find the client ip, and other information that is not present in the request applications have to look up the Channel struct provided by Ace. This is stored in the Process dictionary
There is a slight conflation of terms between config <> initial_state <> state in the server context.
Middlewares cannot communicate with other parts of the stack. I.e. how does a controller interact with the user that was part authenticated by a middleware
Anything that can be memoized in a start callback would require an init style callback.
Current work arounds
To fix (3) middleware, and servers, can make use of the process dictionary. This can be argued for conceptually by saying that we DO want a shared global (to the server/process) state. Some other solutions essentially just put work in to thread this state between all call backs. Raxx Could have helpers
Not really sure what I was thinking here but written down for completeness.
Extend channel.
I really like the fact that servers are arity 2 functions. handle(message, state). In my mind this is the essence of what is happening and extending the list of arguments is just giving special status to parts of the state or the message.
In a Gen.call the from is just another part of the message, although admittedly given special status in the handle_call callback.
def handle_request({request, channel}, state) do
I think it can be argued that middleware is updating the channel.
One passed through an authentication middleware then the inner server can consider that the request is coming from an authenticated channel.
In this model the return values are not updated. A middleware doesn't get to change the channel seen by earlier middleware but it can add to it when passing to inner middleware servers. This means the channel needs to be updated in every callback, when handling streaming.
If a type system with interfaces was available then an inner server could specify that it expects an authenticated channel, at which point it would be a compilation failure if the authentication middleware was not added to the stack.
It might even be possible to trick dialyzer to give this check by using structural typing. I think dialyzer can check maps with required keys. If a middleware is required to add a key then dialyzer might know if it has not been added.
Channel interfaces could also specify the return types, the things that are allowed to be sent to them.
I.e. an error handling middleware expects a result of a response, not a response.
This could extend to general messages. address.send(response) wrapping address can change the things sent as responses.
responses need to be extensible to pass information to higher earlier middleware, Responses should have behaviours all the way to the server when an explicit type is returned.
This separates a kinda implicit circular dependency between middleware adding stuff to the same place for both inner and outer middleware
Channel interface could even specify which bit of the message it is possible to send, but that would require a send function that returned a new channel. linear types blah blah.
Generalising middleware requires a lot from the concept of parametrised callbacks modules, these are only easily handled in elixir "type" system by use of an any type. e.g. a parser middleware breaks the spec that body's are binary.
Other names for channel
from
env
context
client
The channel could have expected fields, helps with adding structure in untyped environment.
e.g.
Unfortunately in Elixir this would require every caller to keep looking through the tuples until it found what it needed.
We could implement behaviours for the request/channel combo but the calling code would always need to check if the middleware had been applied. The case switch of checking if the process had been applied could always be replaced with actually applying the check process.
This could be achieved with prototypical inheritance, a.la. JavaScript.
It would be an interesting experiment to implement prototypical inheritance in Elixir, there is an erlang callback for when a function that does not exist is called on a module, can use that.
Experiment if it's a good idea with a JS based raxx before adding prototypes to elixir :rofl:
Comments around stack
To get rid of the macro solution for Raxx.SimpleServer a controller needs a function that returns a server. I think this function should be called server, rather than init`
# streaming
def server(config) do
{__MODULE__, config}
end
# simple case doesn't return the current module
def server(config) do
{Raxx.SimpleServer, {__MODULE__, config}}
end
This separates config from state, even if it is passed through, this function takes config and returns the first state.
Again if types where a thing the returned server type information could advertise if the stack needed further middleware before mounting on a raw http server.
Non middleware solution
I think the typespecs above might have a mappable relationship to the required types of the channel.
Should investigate if this is true.
Unstructured rambling, maybe crazy
Current Issues,
Channel
struct provided by Ace. This is stored in the Process dictionaryCurrent work arounds
Thoughts
Some messy process dictionary idea
Not really sure what I was thinking here but written down for completeness.
Extend channel.
I really like the fact that servers are arity 2 functions.
handle(message, state)
. In my mind this is the essence of what is happening and extending the list of arguments is just giving special status to parts of the state or the message.In a
Gen.call
the from is just another part of the message, although admittedly given special status in thehandle_call
callback.I think it can be argued that middleware is updating the channel. One passed through an authentication middleware then the inner server can consider that the request is coming from an authenticated channel.
In this model the return values are not updated. A middleware doesn't get to change the channel seen by earlier middleware but it can add to it when passing to inner middleware servers. This means the channel needs to be updated in every callback, when handling streaming.
If a type system with interfaces was available then an inner server could specify that it expects an authenticated channel, at which point it would be a compilation failure if the authentication middleware was not added to the stack.
It might even be possible to trick dialyzer to give this check by using structural typing. I think dialyzer can check maps with required keys. If a middleware is required to add a key then dialyzer might know if it has not been added.
Channel interfaces could also specify the return types, the things that are allowed to be sent to them. I.e. an error handling middleware expects a result of a response, not a response. This could extend to general messages.
address.send(response)
wrapping address can change the things sent as responses.responses need to be extensible to pass information to higher earlier middleware, Responses should have behaviours all the way to the server when an explicit type is returned.
This separates a kinda implicit circular dependency between middleware adding stuff to the same place for both inner and outer middleware
Channel interface could even specify which bit of the message it is possible to send, but that would require a send function that returned a new channel. linear types blah blah.
Generalising middleware requires a lot from the concept of parametrised callbacks modules, these are only easily handled in elixir "type" system by use of an any type. e.g. a parser middleware breaks the spec that body's are binary.
Other names for channel
The channel could have expected fields, helps with adding structure in untyped environment. e.g.
This fixes the issue of removing mount from the request, it's part of the environment.
Binding from the router can also go here.
Dialyzer might be able to track these fields going from nil -> value to check stack
Structs are not composable
Ideally the middlware would do the following process
Unfortunately in Elixir this would require every caller to keep looking through the tuples until it found what it needed. We could implement behaviours for the request/channel combo but the calling code would always need to check if the middleware had been applied. The case switch of checking if the process had been applied could always be replaced with actually applying the check process.
This could be achieved with prototypical inheritance, a.la. JavaScript. It would be an interesting experiment to implement prototypical inheritance in Elixir, there is an erlang callback for when a function that does not exist is called on a module, can use that.
Experiment if it's a good idea with a JS based raxx before adding prototypes to elixir :rofl:
Comments around stack
To get rid of the macro solution for
Raxx.SimpleServer
a controller needs a function that returns a server. I think this function should be called server, rather than init`This separates config from state, even if it is passed through, this function takes config and returns the first state.
Again if types where a thing the returned server type information could advertise if the stack needed further middleware before mounting on a raw http server.
Non middleware solution
I think the typespecs above might have a mappable relationship to the required types of the channel. Should investigate if this is true.
in controller
{params, user_id, etc} = MyApp.do_all_the_stuff(request, channel)
parse + auth + csrf