question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

simplify commands, queries, and middleware

See original GitHub issue

Do you want to request a feature or report a bug?

Improvement / debt.

What’s the current behavior?

Right now we’ve got three separate concepts: middleware handlers, command functions, and query functions. This is fine, but I’ve been bumping up against limitations with it that stem from the distinctions.

It has already been pointed out that commands and queries aren’t any different from a technical point of view. It’s just that some return values and some apply operations. But there’s nothing stopping them from doing the other, or even one from doing both at once. (Technically commands are chainable, but this such a small benefit that shouldn’t prevent better solutions.)

Also, as more and more logic is ported to editor-level queries, to allow it to be customizable in userland for more advanced editors, the special-cased middleware handlers start to seem odder and odder. For example, what is renderBlock if not a query that returns React elements? And what is onKeyDown if not a special command that is used in DOM environments.

I think we’d stand to gain in simplicity by combining these concepts.

The only difference right now is their arguments ordering, which presents a problem. Each one is slightly different. The middleware take the editor last:

(props, editor, next)
(event, editor, next)
... 

Whereas the queries and commands take it first:

(editor, range)
(editor, point)
...

But this brings us to a second issue that is mentioned in https://github.com/ianstormtaylor/slate/issues/2466 and in https://github.com/ianstormtaylor/slate/issues/2342 that is that the middleware stacks are awkward from a few standpoints—especially in terms of debugging.

I originally borrowed the middleware concept from Koa which seems to have solved it nicely. But having played with Micro, I’ve come to realize that using a next() pattern is actually just a leftover in the API from before async/await. And since we don’t have the asynchronous issue—and even if we did async/await can solve it—we can eliminate the next() pattern and use full composition instead. Micro does this to a really nice effect.

The only major difference is that Micro is concerned with a single stack, whereas we need to allow a huge number of functions to be composed and overridden.

But that’s not impossible, it just means that we need to use a dictionary of functions instead of a single one. Which would end up looking very similar to our existing plugin definitions for middleware handlers:

{
  onKeyDown: fn => (event, editor) => {
    if (...) {
      ...
    } else {
      return fn()
    }
  }
}

But this still leaves the problem of the ordering of editor. Since we like keeping props and event in the first position, to mimic React’s event handlers and render prop patterns.

However, we can actually move the editor itself to the set of composed arguments too, since it never changes once a plugin is initialized. So we’d get:

{
  onKeyDown: (fn, editor) => event => {
    if (...) {
      ...
    } else {
      return fn()
    }
  }
}

Which leaves us with middleware, commands and queries all having the same signature:

{
  [name]: (fn, editor) => (...args) => {}
}

Meaning that plugins can be simplified to being just a dictionary of composition functions. Commands become middleware. Queries become middleware. And the React-specific commands and queries like onKeyDown and renderBlock are treated no differently.

(This requires extracting schema into a separate plugin, which we’ve already wanted to do from https://github.com/ianstormtaylor/slate/issues/2333.)


Further, since fn is actually the composed function itself, it gives us two more benefits that we are awkward with out current middleware setup:

  • Changing the arguments passed to a function is clear, you can just change what you call fn(...) with, and it won’t know the difference. This allows for more powerful overrides.

  • Debugging is simpler, since you can step directly into the composed function, and continuing going all the way down the stack. Instead of stepping into the awkward middleware looping logic as happens now.


Moreover, this allows us to simplify the methods that Editor exposes. Right now since things are slightly different we have:

editor.command(type, ...args)
editor.query(type, ...args)
editor.hasCommand(type)
editor.hasQuery(type)
editor.query(type, ...args)
editor.registerCommand(type)
editor.registerQuery(type)
editor.run(type, ...args)

Which could then be simplified to:

editor.exec(name, ...args)
editor.has(name)

(This leaves open another question of whether it’s better to have plugins be simple dictionaries of composition functions, or for them to a function that is passed the editor to use an imperative API like editor.register(name, fn). But we can choose to change this later if we discover it has benefits.)

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:5
  • Comments:11 (8 by maintainers)

github_iconTop GitHub Comments

2reactions
justinweisscommented, Jun 10, 2019

Yeah, I think it’s just that I’m used to it, and have a lot of functions that are using that style. I suppose it would be easy enough to emulate in plugin-land if I needed it:

{
  command: (fn, editor) => (commandFn, ...args) => {
    commandFn(editor, ...args);
    return editor;
  }
}

editor.command((editor, arg1, arg2) => {...})
1reaction
justinweisscommented, Jun 10, 2019

Without a small layer in between I’m not sure how we’d solve this?

Yeah. What I meant was, it’s something we could solve later by wrapping fn(). You’d have a wrapper layer in between each plugin layer, but no client code would have to change, and the backtrace would still be pretty readable – at least, way more readable than it is today. Sounds like we need that immediately, though 😃

Not exactly sure what you mean here?

Not really important, just agreeing with you that using return values from plugin functions will be flexible. Although it’s a shame we lose chaining because of it! Don’t think it’s possible to have both, though. And it’s not like I would have missed chaining if it hadn’t existed to begin with.

You’d still be able to run all commands as editor.insertText directly with the new system. The registering logic being exposed was some edge case I can’t recall. Does that solve your worry there?

Yep, I think so.

Commands/queries in the new system would all receive the “old” function they are composing (essentially next) so yup. Does that eliminate your worry about having onExec? I wasn’t planning on keeping it, since I’ve only ever used it for overriding commands, which is now built in.

That solves all of the places I’m using onCommand / onQuery for right now. I still think it would be nice to be able to intercept all commands in a plugin. Since I can’t think of a specific case where I’d actually do it, it’s probably not necessary.

Open to it! I think it used to be called that. Only thing I’m thinking is paralleling the DOM with execCommand naming. Small pros/cons either way, so we can revisit this in the future if we decide call is better.

Cool. Mirroring some existing thing was what I was going for, I couldn’t think of what exec was meant to mirror.

Oh, something that’s not clear to me yet – are you planning to have exec work with anonymous functions, the way command does today? What would their signature look like?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Complete Guide to Express Middleware - Reflectoring
Express middleware refers to a set of functions that execute during the processing of HTTP requests received by an Express application.
Read more >
CQRS and MediatR in ASP.NET Core - Code Maze
We will see MediatR in action with the requests, notifications, ... CQRS stands for “Command Query Responsibility Segregation”.
Read more >
Command Query Responsibility Segregation (CQRS) pattern
This separation of APIs is an application of the Command Query Separation (CQS) pattern, which separates methods that change state from the methods...
Read more >
CQRS is simpler than you think with .NET 6 and C# 10
CQRS is a pattern where we're segregating application behaviours. We're splitting them into command and queries. Commands are intents to do ...
Read more >
Route-Based Middleware to Handle Default Population Query ...
Query Logic in Middleware ... Now that you know how to build useful queries, you can look at optimizing the process further by...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found