JoshMarler / react-juce

Write cross-platform native apps with React.js and JUCE
https://docs.react-juce.dev
MIT License
763 stars 79 forks source link

Provide data/event flow examples and facilities #66

Open JoshMarler opened 4 years ago

JoshMarler commented 4 years ago

Currently ReactApplicationRoot and EcmascriptEngine support pushing events from native/C++ code into JS/React via the dispatchEvent mechanism.

dispatchEvent is a function registered/created against the global __BlueprintNative__ object by EventBridge.js.

It seems that a lot of apps and client code would benefit from a generic "event based" framework or pattern to communicate between C++ and JS.

For example, it would be nice to both send an event (consisting of an ID and payload/args) from C++ to Javascript and from Javascript to C++.

I might wish subscribe to a "parameterChanged" event in JS and also trigger/signal that same event back to C++.

Is this something that blueprint could/should offer via EcmascriptEngine/ReactApplicationRoot?

I could imagine a complimentary function to ReactApplicationRoot::dispatchEvent along the lines of ReactApplicationRoot::Listener::handleEvent.

i.e.

virtual void ReactApplicationRoot::Listener::handleEvent(const juce::String& eventType, const juce::NativeFunctionArgs& args) = 0;

(ReactApplicationRoot::Listener is purely there to illustrate a point, could be a lambda callback or whatever)

This might require some change or "fleshing out" of EventBridge to support a publish and subscribe like interface.

The big initial question feels like "Should Blueprint provide this or should examples be given with data-flow patterns/implementations left to the users of Blueprint?"

@nick-thompson I'd love to hear your ideas around this area. I'm going to implement some sort of event based communication in my app over the coming days so will let you know if I come up with any interesting stuff.

For me it would be super useful just to have an inverse dispatchEvent function that I can call from JS and listen to in C++. We have plans to build a generic data model/controller system off some sort of simple pub/sub mechanism.

I don't think that dictates design patterns to users too strongly as there are plenty of other approach people could use but it does give a tool that various patterns can be built off of.

This could also work really nicely with a ValueTree where events are id'd to match a value tree property identifier. A component owning ReactApplicationRoot could then listen to events from JavaScript to update its value tree. Said component could also listen to the value tree itself and dispatch the same event to JavaScript, allowing straight forward 2 way data binding with an application's data model.

The ValueTree approach would allow super straight forward undo/redo behaviour in apps managed with ValueTree as undo/redo would automagically update the JSUI's controls. A tonne of if/else comparators on property identifiers could be avoided if the events received from JS could just map straight to ValueTree::setValue(event.id, value). C++ code could also listen to the ValueTree::valueTreePropertyChanged callback and push the value of the changed property straight into JS using dispatchEvent. It feels like this could provide a nice generic system for many apps. Might need some careful thought to try and apply as much strong typing as possibly as this will be throwing around a lot of string id's and NativeFunctionArgs.

The ValueTree event based approach should be interoperable with AudioProcessorValueTreeState to provide a solution for both plugins and larger standalone apps. We could probably provide some sort of helper class for this and offer it as a basic "data flow" tool. People could choose whether or not to use it in their applications. Not everyone is going to want this but I think it could provide some real value.

Dave Rowland did a great talk on managing large scale apps with juce::ValueTree. Might make a nice reference.

JoshMarler commented 4 years ago

@nick-thompson, in the middle of implementing two-way event dispatch at the moment as a test, so I can open up a PR if you like the sound of this idea?

EventBridge could end up becoming a class with your typical pub/sub pattern. Something like the below (probably more elegant though :-)):

class EventBridge {
  constructor() {
    this.subscribers = [];

    __BlueprintNative__.dispatchEventFromNative = (event, data) => {
      if (!subscribers[event]) return;
      this.subscribers.forEach((subscriber) => {
        subscriber.callback(event, data);
      });
    }
  }

  publish(event, data) {
    __BlueprintNative__.dispatchEventToNative(event, data);
  }

  subscribe(event, callback) {
    if (!subscribers[event]) {
      subscribers[event] = []
    }

    let index = subscribers[event].push(callback) - 1;
    subscribers[event] = { index: index, callback: callback };

    return index;
  }

  unsubscribe(event, subscriberIndex) {
    if (!subscribers[event]) return;
    subscribers[event].splice(index, 1);
  }
}

Obviously there are pros and cons to be weighed up around "Stringly Typed" event based systems.

In regards to making EventBridge a bit more elegant, I'm assuming you could use EventEmitter still, I'm not super familiar with it. What would be the best pattern to allow client code to call both EventBridge.emit("beginParameterChangeGesture") and EventBridge.on("beginParameterChangeGesture", callback) without self notifying etc.

Something along the lines of this?

class EventBridge extends EventEmitter {
  constructor() {
    super();

    __BlueprintNative__.dispatchEventFromNative = (eventType, ...args) => {
      this.emittingFromNative = true;
      this.emit(eventType, ...args);
      this.emittingFromNative = false;
    }
  }

  // Override EventEmitter.emit()
  emit(eventType, ...args) {
    if (this.emittingFromNative) {
      super.emit(eventType, ...args)
    }
    else {
      __BlueprintNative__.dispatchEventToNative(event, ...args);
    }
  }
}

Where dispatchEventToNative might look a bit like the below. Where eventHandler is a public assignable std::function object on ReactApplicationRoot.

void registerNativeEventHandler()
        {
            engine->registerNativeMethod("__BlueprintNative__", "dispatchEventToNative", [](void* stash, const juce::var::NativeFunctionArgs& args) {
                auto self = reinterpret_cast<ReactApplicationRoot*>(stash);
                jassert (self != nullptr);
                jassert (args.numArguments >= 1);

                const juce::String eventType = args.arguments[0].toString();

                std::vector<juce::var> eventArgs;

                if (args.numArguments > 1)
                {
                    for (size_t i = 1; i < args.numArguments; ++i)
                    {
                        eventArgs.push_back(args.arguments[i].clone());
                    }
                }

                jassert(self->eventHandler);
                if (self->eventHandler)
                {
                    self->eventHandler(eventType, juce::var::NativeFunctionArgs(
                                        juce::var(),
                                        eventArgs.data(),
                                        eventArgs.size()));
                }

                return juce::var::undefined();
            }, (void *) this);
        }
JoshMarler commented 4 years ago

@nick-thompson, FYI I've got this working locally and so far seems pretty good.

Have some ideas for some utility classes that basically hook up a ReactApplicationRoot instance with a ValueTree based data model, to allow instantly binding events back and forth. Thinking about best ways to introduce some type safety at the moment.

It also strikes me that two way dispatch using NativeFunctionArgs that allow dispatching an event whose args include a DynamicObject would be super useful.

Would be nice to add a dispatchEvent overload to ReactApplicationRoot which take a juce::NativeFunctionArgs etc. Obviously requires a fair bit of thought.

nick-thompson commented 4 years ago

Hmmm a lot to think about here, thanks for starting the conversation @JoshMarler.

The big initial question feels like "Should Blueprint provide this or should examples be given with data-flow patterns/implementations left to the users of Blueprint?"

I think that's exactly the right place to start. I suppose my initial stance is that Blueprint itself shouldn't take a stance on data flow patterns, because every app is different and everbody has their own opinions on data flow patterns. In React you see a ton of Flux/Redux, but there are plenty of people who prefer an RxJS style approach, for example. However, I think Blueprint should provide some set of facilities for easily implementing your data flow pattern of choice.

EventBridge was an early idea and very much only a partially formed idea. It came about as I was building a Flux pattern for Remnant in Blueprint, where my one true data store was the AudioProcessorValueTreeState, all changes were broadcast as events into js land, and from js there were no direct ways to manipulate state, only to issue "actions," as in the Flux model, such as "parameter value update." That's why you only see the C++->JS direction. That's nice for Flux and for audio plugins, but I can imagine many apps even with a similar data flow pattern where you'd want the "one source of truth" living in js land and broadcasting change events to C++. So, thinking about that more now, I think your idea for building out EventBridge into a true pub/sub system that can go either direction is the right move.

I think that also provides a good opportunity to implement something I've done a few times but not quite yet integrated fully: a coherent way to throttle events going from C++->JS. Right now the GainPlugin calls into js on every parameter value change, and this event comes in all the time during a drag gesture or during host automation. I implemented what I called a "throttle map" (https://github.com/nick-thompson/blueprint/blob/master/blueprint/core/blueprint_ThrottleMap.h) that I think we could integrate into the EventBridge from C++->JS so that as you dispatch events you can choose if they should be throttled or not. Need to spend a minute on what that API might look like from the ReactAppRoot side of things.

Anyway, then carrying on to the automatic ValueTree 2-way sync– I think that's an awesome idea, but my initial reaction on it is that that falls into the "taking a stance on data flow patterns" category (at least, 2-way data binding is often quite at odds with the Flux model). Which makes me think that maybe it shouldn't be part of blueprint core (but absolutely should be a well documented example at least). I need to spend a little more time on it, and if you have some code ready I'd love to see it, that'd be quite helpful. Either way, I think you're totally right that many people would find that super helpful, and it would apply to many apps.

Hope that was a sensible ramble. I'll think on it some more, revisit tomorrow and look more closely at the API design side of this

JoshMarler commented 4 years ago

Hey Nick,

Thanks for the discussion. Yep fair enough, I think I agree. I'd be really pleased just to see the two way pub sub mechanism there. I have that working at the moment but it's just a bare bones implementation for me to leverage. I'll think about the API some more today and maybe do the usual "open up a PR for Nick to fix :-)"

I've been down the redux route a couple of times in the past and found it "a bit wordy". I've more experience with Angular and at the time (when I was actually writing some JavaScript) it had a fairly opinionated rxjs approach which I quite liked.

I'm still very much in the exploratory phased with this stuff so I'll probably try a few different patterns. Flux is a thing I should probably catch back up on.

A value tree like system appeals as I'm working with a pretty big application with LOTS of events. Being able to bind events straight into some sort of "model" interface where each event ID maps to a model/ValueTree property key could save us a lot of repetitive boiler plate.

However, totally possible this approach is going to be too fixed in some cases and result in too much runtime type checking.

I suspect I'll also need multiple options so I agree, Blueprint probably shouldn't prescribe any ValueTree based approach, it should.just provide thing like the EventBridge to facilitate any pattern users might take. ValueTree binding will probably just make a nice example in the docs.

nick-thompson commented 4 years ago

I'd be really pleased just to see the two way pub sub mechanism there. I have that working at the moment but it's just a bare bones implementation for me to leverage. I'll think about the API some more today and maybe do the usual "open up a PR for Nick to fix :-)"

Yea same, I think the two-way pub sub is definitely the right move. Please do share some code when you have it! I'll spend some time thinking on the API myself as well. I feel like it could really be quite simple

I've been down the redux route a couple of times in the past and found it "a bit wordy".

Definitely. I'm personally not a huge fan of redux, but so many people are. I often write an as-minimal-as-possible Flux pattern (as in the GainPlugin example) for my state management for smaller apps.

A value tree like system appeals as I'm working with a pretty big application with LOTS of events. Being able to bind events straight into some sort of "model" interface where each event ID maps to a model/ValueTree property key could save us a lot of repetitive boiler plate.

Right, yea, I can imagine many cases where this auto-enumerating event binding makes tons of sense. And this morning as I'm thinking through it more, while I do still think that Blueprint should probably only provide the EventBridge to facilitate this, this kind of value tree event dispatcher/synchronizer might make a great add to the "Extras" library in #53. Especially if our extras library will include bindings to juce-specific things like the juce::Slider, bindings to a juce::ValueTree seems to make a lot of sense there. I'm picturing something like:

class MainComponent   : public Component
{
public:
    //==============================================================================
    MainComponent()
        : treeSync(appRoot, state) // Automatically sets up listeners to call `appRoot.dispatchEvent` and receive events from js
    {
        addAndMakeVisible(appRoot);
        setSize(400, 300);
    }

    ~MainComponent();

    //==============================================================================
    void paint (Graphics&) override;

    void resized() override
    {
        appRoot.setBounds(getLocalBounds());
    }

private:
    //==============================================================================
    juce::ValueTree state;
    blueprint::ReactApplicationRoot appRoot;
    blueprint::extras::ValueTreeSynchronizer treeSync;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Curious if that's along the lines of what you've already written? I'm obviously hand-waving but the thought is that, since this lives on top of the EventBridge layer, it could very much be something that people take off the shelf if they want it, or leave it if they want some specific Redux/Flux/RXJS/Etc

JoshMarler commented 4 years ago

@nick-thompson, Yes! Totally. The extras module sounds like a great place for a ValueTreeSynchronizer. I'm literally working on things like this right now.

I've got a very simple two way EventBridge in js which is working just fine but needs a little more thought. I'm experimenting with the dispatchEvent interface at the moment so I'll have something to send over soon. I'm also building some JS components which can hook into this whole system so the "extras" module feels like something that could definitely start taking shape soon.

Two-way EventBridge:

import EventEmitter from 'events';

class EventBridge extends EventEmitter {
  constructor() {
    super();

    this.emit = this.emit.bind(this);

    this.emittingFromNative = false;

    // TODO: Make constructor arg/configurable?
    this.setMaxListeners(30);

    __BlueprintNative__.dispatchEventFromNative = (eventType, ...args) => {
      this.emittingFromNative = true;
      this.emit(eventType, ...args);
      this.emittingFromNative = false;
    }
  }

  // Override EventEmitter.emit()
  emit(eventType, ...args) {
    if (this.emittingFromNative) {
      super.emit(eventType, ...args);
    }
    else {
      __BlueprintNative__.dispatchEventToNative(eventType, ...args);
    }
  }
}

const _singletonInstance = new EventBridge();
export default _singletonInstance;

Can totally send over a PR soon with the pub/sub stuff. ValueTree utility class might take me a little longer :-) We aren't actually using ValueTree at the moment so I've bound events to our own "data-model" type of class but the pattern follows the exact same principles that binding to a ValueTree would. I'm experimenting with a couple of different data-model interfaces and ValueTree is the next on my list to tackle so if I get something nice together I shall send it over to you.

Cheers.