CrowdHailer / raxx

Interface for HTTP webservers, frameworks and clients
https://hexdocs.pm/raxx
Apache License 2.0
402 stars 29 forks source link

Experiments in expanding the meaning of a Channel. #155

Closed CrowdHailer closed 5 years ago

CrowdHailer commented 5 years ago

Unstructured rambling, maybe crazy

Current Issues,

  1. 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
  2. There is a slight conflation of terms between config <> initial_state <> state in the server context.
  3. 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
  4. Anything that can be memoized in a start callback would require an init style callback.

Current work arounds

Thoughts

Some messy process dictionary idea

{value, new_shared} = put_user(shared, user)
^new_shared = Process.get()

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

The channel could have expected fields, helps with adding structure in untyped environment. e.g.

%Raxx.Channel{
server: Ace,
authority: user_id, # alt as:
secure: true,
session: %{}

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

{request, channel} -> {{request, channel}, auth_infor}

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.

in controller {params, user_id, etc} = MyApp.do_all_the_stuff(request, channel) parse + auth + csrf