7.4.0: add store.use() middleware #283
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds a store.use(tableId, handler) middleware API that enables business rule enforcement on both local mutations and CRDT sync. This addresses a fundamental limitation identified in #280: listeners cannot prevent invalid data from entering the store—they only observe changes after they're applied.
The .use() format is inspired by Express: hopefully that provides some familiar ergonomics.
There's basically two scenarios I could see people using Tinybase in: client/server and p2p nodes. In both cases, middleware provides a necessary function. In the case of a server it allows you to validate your incoming data. And in the case of p2p nodes: validations are equally important, if not more.
Some really nice patterns result from this, for instance:
This allows us to create error handling which runs on both client and server (or all nodes in the p2p context). The client blocks bad data and allows the UI to render the error. And in the case where the validation needs to run on the server (it needs access to priveledged data such as cryptographic keys for instance) then the server has the opportunity to bubble errors up through the same store (perhaps tagged with {source: 'server'}).
Middleware gives us a place to create these patterns while also being flexible for an inumerable amount of future use cases. The key innovation here is that this prevents bad data from getting into the model, and prevents peers from accepting bad data over the wire.
How did you test this change?
I added a new test file specifically for middleware.ts. Many additional tests are added.
Risks
It's definitely possible to write infinite cycles using this. That could be a footgun. That's also true of mutating listeners though. I think the only way to protect the user would be through some kind of cycle detection. I'm not sure how you would do that other than code analysis.
Implementation
I tried in every way to take a light touch. I thought at first that we could do this by a small change to MergeableStore. Alas, I later realized:
I did my best to stick to the code idioms of the project. The bulk of the code changes are in two files: middleware.ts and middleware.test.ts.
I did run over 6000 tests and I don't think we have regressions, but it's hard for me to say because running the full test suite on my laptop consumes all available memory and hard crashes my laptop. This was true as well in the unaltered codebase before any of my changes. I presume you have CI, or a beefier machine at your disposal.
Peace. I know this is a big change, so let me know if there's anything I can do to make it more digestible.