Skip to content

Conversation

@bitmage
Copy link

@bitmage bitmage commented Jan 6, 2026

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:

store.use('jobs', (rowId, cells) => {
  if (!cells.name || typeof cells.name !== 'string') {
    store.setRow('errors', `err-${rowId}`, {
      table: 'jobs',
      rowId,
      message: 'Job name is required',
      timestamp: Date.now(),
    });
    return null; // reject the row
  }
  return cells; // accept
});

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:

  1. I needed to improve the ergonomics by iterating through the change set and presenting a row-based interception of values
  2. modifying MergeableStore wasn't sufficient, I also needed to modify Store in order to prevent local additions

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.

@jamesgpearce
Copy link
Contributor

Wowzers! Let me get to this amazing contribution...

@bitmage
Copy link
Author

bitmage commented Jan 8, 2026

@jamesgpearce are you a Clojure guy? Your coding style reminds me of Clojure or some dialect of Lisp. We may share some inspiration: Clojure and FP were what originally got me turned onto Javascript, around 15 years ago.

@jamesgpearce
Copy link
Contributor

Haha not particularly! This started from an obsession with reducing the file size, which means that I start to (pathologically!) functionalize more and more verbose boilerplate idioms... BTW don't worry about the house style in PRs... I can always wrap things up later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants