alohaeditor / Aloha-Editor

Aloha Editor is a JavaScript content editing library
http://www.alohaeditor.org
Other
2.46k stars 535 forks source link

Current aloha middleware concept insufficient #1292

Open deliminator opened 9 years ago

deliminator commented 9 years ago

Currently what you have is

var event = handlerA(handlerB(handlerC(event)))

This doesn't allow middleware to stop other handlers from executing, and it also doesn't allow middlewares to keep local state in-between events (which is sometimes useful if done tastefully).

A simple middleware that counts events it processed and stops after 1000 processed events could be defined like this:

return {
    middleware: function (next) {
        var eventsProcessed = 0; // tasteful local state
        return function handler(event) {
            eventsProcessed += 1;
            if (eventsProcessed < 1000) { 
                return next(event);
            }
            return null; // not sure what the result of event processing is
        }
    }
};

The composition of these middlewares could be defined like this

var handler = moduleA.middlewareA(moduleB.middlewareB(moduleC.middlewareC(Fn.noop)))
function handleEvent(event) {
   var result = handler(event);
   ...
}
petrosalema commented 9 years ago

I wonder whether permitting middleware handlers to abort the execution of subsequent handlers based on their local logic is advisable.

Middleware affecting control flow outside of it's own scope seems a little smelly to me.

Is it not better that each handler should always have the opportunity to determine whether or not it should execute it's logic given a set of conditions that are provided in an uniform event object and (potentially) it's own state?

Thus a handler can be defined like this:

var middlewareB = (function () {
      var eventsProcessed = 0; // tasteful local state
      return function handler(event) {
          // abort on condition establish in preceding handlers 
          if (event.middlewareATerminalState) {
              return event;
          }
          eventsProcessed += 1;
          if (eventsProcessed > 1000) {
              // visible to subsequent handlers
              event.middlewareBTerminalState = true;
          }
          return event;
      };
}());

One wishing to change the condition on which middleware logic is executed needs only to replace handlers with wrap versions of those handlers.

deliminator commented 9 years ago

In the second example, middlewares need to be aware of whether they can be aborted or not. You can't abort middlewares that haven't implemented abortability.

Since you can implement the editor stack however you want (editor function in aloha.js), you can still abort middleware that doesn't implement abortability, but it can't be implemented anymore in the middleware, it has to be implemented as part of aloha.js. 3rd party aloha extensions/plugins would be forced to inform the implementer of aloha.js to abort other middlewares, and under what condition, and can't just say "just wrap your existing stack with this function and we will do the right thing".

Allowing a middleware to abort other middlewares allows it to control the execution flow. Below is an example that implements branching:

return {
    middleware: function (handleSomeCondition, handleOtherCondition) {
        return function handler(event) {
            if (event.someCondition) { 
               return handleSomeCondition(event);
            } else if (event.otherCondition) {
                return handleOtherCondition(event);
            } else {
                return event;
            }
        }
    }
};

In the above example, handleSomeCondition is what would be usually called next. That you usually only have a single next is just a convention. But there are valid cases where you want to impement the event pipeline in a way that is different based on some state in the editor, for example based on a button the user clicked.

On top of this you can implement arbitrary middleware DSLs. You don't need any logic in aloha.js, at all. Everything cal be implemented in a middleware, since the outermost middleware controls every aspect of all the wrapped middlewares, even whether it's executed or not.

Even the editor function in aloha.js that currently implements the composition of all middlewares should be a middleware. A middleware that accepts a list of handler-functions instead of middleware-functions-that-return-handler-functions - it's a perfectly valid case for a middleware, since some logic implemented by aloha extensions/plugins don't need to be full middlewareware-functions-that-wrap-handler-functions and can be instead just be handler-functions.

If you agree that aloha.js' current implementation of the editor function could be a middleware, and not implemented as part of aloha.js, then I think we are in agreement, since you can always compose simple handler-functions with the editor middleware, and you can compose the result of the editor middleware (which is a handler-function) with other middlewares that want to control execution flow.

By the way, the middleware pattern I proposed is not somthing I invented. It's a pattern I saw used in clojure's ring library. Ring provides a rich http-handling DSL, something I hope can be adopted for Aloha's event processing. With the ring library, handlers aborting other handlers have not been a source of smell as far as I can judge.