FamilySearch/fs-js-lite

Middleware

Closed this issue · 5 comments

While implementing #1 it occurred to me that supporting middleware would create a cleaner interface for optional features and make possible future enhancements easier (such as logging and caching).


Middleware could be registered when the client is created

var client = new FamilySearch({
    middleware: [middleware1, middleware2]
});

or added later

client.addMiddleware(middleware2);

Middleware would be called in the order they were added when processes requests and called in reverse order when processing responses.

Middleware would be an object with request and response properties that are both a function with the following signatures

{
    request: function(request, next){ },
    response: function(request, response, next){ }
}

Does middleware need access to the client instance? I could see it being necessary. I see two obvious options.

Parameter

Pass the client object as the first parameter to the request and response methods.

{
    request: function(client, request, next){ },
    response: function(client, request, response, next){ }
}

this

Or bind the client to this when calling request and response.

{
    request: function(request, next){ 
        var client = this;
    },
    response: function(request, response, next){ }
}

A downside of binding this is it limits how middleware can be implemented; you couldn't use instances of prototypes for middleware.

If we want request middleware to be able to modify the request then they can't have access to the XMLHttpRequest object because you can't change it's values. Instead the request middleware will have to be given the request options before the XMLHttpRequest is created.

Request middleware needs the ability to cancel or short-circuit the request chain. Caching middleware would return a request and thereby terminate the remaining request middleware, skip XHR, and proceed to response middleware. Response middleware needs to be called for the possibility of logging and response transformations. Caching would also have a response handler which would need to account for possibly receiving cached responses.

Response middleware doesn't need to terminate the chain but it needs the ability to issue new requests (e.g. throttling) which it gets for free; we can't stop it. Nonetheless the user only sees one response.

I had previously stated that response middleware would be fired in reverse order of request middleware. That idea assumes that most middleware will have both a request and response components, which may not be true. It also makes middleware more difficult to reason about. Instead we will execute both in order which makes it easier to reason about and control for developers. If developers want the middlware to be in FILO stack order then they can set it up that way.

Middleware will be added individually client.addRequestMiddleware(middleware) and client.addResponseMiddleware(middleware).

It may be useful to have client.addMiddlware() like I mentioned above but we won't add that until we have a solid usecase. Adding middleware as one component would still require a way for the middleware to register it's request and response components so the developer might as well do that in the order they please.

When response middleware issues a new request it should pass on the new request and response. In that case it ought to cancel any succeeding middleware for the old response. The only way I know to achieve that is by having next() mean continue and next(anything) mean cancel.