Skip to content

Conversation

mmkal
Copy link

@mmkal mmkal commented Sep 14, 2025

Hi 👋

We're using jsonata to allow our customers to write custom matchers for the state of an object. As it stands right now, this means the place where we call jsonata(customerThing.expression).evaluate(ourData) we have to add await. Which means that function has to become async, and we ideally don't want that. We don't want to enable any custom functions, and certainly not async ones. So we'd prefer to have a synchronous version of jsonata. And looking at the code, I noticed that almost none of it needs to be asynchronous for any reason other than it might call some asynchronous user-defined function.

So, this PR uses a really dumb script to create a whole separate submodule of the package (under ./sync) which:

  • uses regex to replace async and await with /* async */ and /* await */
  • uses regex to replace Promise.all(...) with Array.from(...) (pointless calling Array.from on something that's already an array, but avoids complicated syntax modification
  • replaces all if (isPromise(result)) { ... } with if (isPromise(result)) throw new Error('no promises pls')

It also copies over all tests, modifying them to use the sync version of jsonata instead:

  • skips 4 tests + the async-function.js tests which are specifically for time-based and user-defined async function
  • runs and passes all 1733 other tests(!)

Not sure if you'll want to accept and maintain this - the implementation right now is a hack. But it seems pretty valuable to provide an API that doesn't require calling from an async function. So, opening a PR if for no other reason than as a PSA - it can be used right now, at least from node, by adding "jsonata": "github:mmkal/jsonata#syncify.js" to package.json.

Note: a library like quansync might be a good option for supporting both sync and async without having to duplicate the code.

@mmkal mmkal mentioned this pull request Sep 14, 2025
@Ghoughpteighbteau
Copy link

I recently spent two weeks attempting to integrate jsonata queries into handlebars.js templates and did not have a good time.

handlebars.js have made a decision to remain synchronous, with the reasonable argument that: "We're doing a synchronous operation, why should we run asynchronusly? Run your long running operations first." ... They have a point. Handlebars.js is not an executor. It really is a synchronous operation. Their focus was on template rendering performance and they weren't going to accept any degradation for a use case that isn't in scope.

The challenge is that many libraries, jsonata.js among them, have chosen to be async, making integration basically impossible. Interestingly, both projects faced the same question: "what kind of custom functions should we accept?" and arrived at opposite answers:

  • Handlebars.js: we are focused on performance, and long running operations are out of scope.
  • jsonata: we should run async because what if the user needs to run something async?

I've reviewed @mmkal's work here and I agree with them, that is a dirty hack. But hey it passes the test suites. There's a similarly dirty hack on the handlebars.js side where someone modified the template compiling code to just add async awaits to everything. That actually does break some things, but also people are using it in production environments right now because people are trying to integrate things like this.

The taint of async spreads to all that it touches unfortunately, and it's no easy task to contain it.

I would not blame the maintainers for rejecting this pull request. But the need for it really is there, or at least I think so... and in defense of @mmkal's work. If the sync version really passes all the tests. And it's still not acceptable for release, then is it the hack that's the problem? or are tests incomplete?

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