-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate Observables to coordinate push-based Dataflow (with reactive properties) #444
Comments
A functional representation of the eventbus would be "observables". It'd be interesting to compare if one could create observables on the domains and use that concept instead. |
See: https://stackoverflow.com/a/47214496/582917
And with that being said, I think I finally understand where rxjs would play a role in PK. (I already like the idea of ixjs too since we already have alot of pull-oriented APIs using async iterables and iterables). |
I'm going to commandeer this issue to talk about Basically PK will have a push AND pull data flows going on, as most applications do. This flow is like a "circle".
|
There are only 2 uses of the event bus atm:
So integrating It's important that there's a way to divide observable objects down to smaller observable things. So if there's an observable structure, I want to be able to "divide" it into its observable properties. And I should be able to build up small observable properties to larger observables, to the point that the entire domain would be observable too. |
Some references regarding observable properties in a class, and the observable class itself:
Atm, it's easy enough to make a property observable. But what if you want the whole object/class to also be observable. This currently seems kind of vague. I asked it here: ReactiveX/rxjs#6157 (comment) |
It seems that one shouldn't be extending Therefore it seems that you mostly make the properties observable. That being said, it's possible to create a "composed" observable similar to rxdb. For example in rxdb, the entire database can be observed by doing: Therefore it seems that a viable design is something like:
The Then there's a matter of what exactly you're subscribing to. Would However one has to trigger the |
Some useful discussion about making our classes observable too: garretpremo/rxjs-observed-decorator#10. Note that I'm also thinking there are going to be 3 kinds of observables here:
I imagine one shouldn't acquire history from stateful observables, just from this point onwards. If you want to sync the current state, you should have called that first, while acquiring the change stream, and mapping that change stream. There is a sort of a race condition here that should be prevented by ensuring that you don't lose change data while you're acquiring the current state. This might even help with CQRS subsequently. At the end of the day propagating such a structure to the frontend. This means we may have decorators for the class, decorators for properties. What about decorators for "updating" methods? Those are the "origin" of change. Imagine:
But you can't just decorate the async generator. Cause you'd be holding a transaction there while you are reading (and you don't really want to do that). Furthermore, that would be a pull based API. Instead |
I've done a bit of research into how to use rxjs. I think we can do this now, it's actually not that big of a change. The main thing is to provide decorators that reduce the boilerplate for creating "dynamic"/"reactive" properties, and being able to wire that up to a reaction on the entire domain object. The other thing is the way it interacts with This means there may be reactive properties that are "static", and reactive properties that are only acquired through the getter. Subsequently downstream classes can subscribe to them on The usage of The usage of We'll focus on the integration of |
I've also noticed that we are still using
We should also be replacing the This should also be applied to the EFS. Although I'm not sure about that since it is mean to "replicate" the behaviour of Node streams (although it may just be fine to use the Node streams instead of But PK itself should be using web streams. |
Note that rxjs is designed for push-based dataflow. So since our streams/RPC is focused on pull-based dataflow. The expectation is that between processes/agents there will be pull-based flow. But internally a pull-flow can be converted to a push-flow that triggers reaction points in other parts of the application. This should be considered in relation to the RPC deisgn. |
As we go to more abstract, we can more flexibility and in particular on level 3 we can some stronger type safety. One thing that's peculiar about level 3 abstraction is that it unifies the data-flow and control-flow. As in data-flow can be used to trigger control-flow on a higher level. For example in our experience in retool, when a change occurs in module 1, and that needs to trigger another thing in module 2, it's not possible to use level 0, 1, or 2. Instead one must export a reactive variable from module 1 and pass it into module 2 where module 2 will subscribe to these changes. So the reactive variable itself is dataflow, however the dataflow itself is being used to express logic, if the intention is for module 2 to perform some other logic as part of a larger control flow abstraction. For example module 1 might be writing something to a DB, then needs to trigger module 2 to fetch something from the DB. Note that the above are general patterns that you can see, the exact implementation may vary and have different trade-offs. But you can see when tends to be expressed with observables, vs something that is expressed with event emitters and pub/sub, vs something that is expressed with injected callbacks executed at hook points, vs something that is just hardcoded jumps. In our code base we have used level 1, 2 and 3 in varying places. Usually level 1 is used when the module is relatively simple. Level 2 when we need more flexibility. Level 3 hasn't been taken advantage yet in Polykey, and in particular I think this would be relevant moving ahead, and into PKE too. Level 3 is something that would be necessary in networked-scenarios. If you think about it, webhooks is an example of level 1 and level 2. They are level 1 when you are interacting with them directly, since you basically have to assign callbacks up front, and then there's no dynamic flexibility afterwards. Level 2 when we think about how many different clients/customers may be using the same system to do a webhook. But they definitely aren't level 3. This is because there's no standardised notion of a reactive variable over HTTP, we just have various stream protocols, and they all tend to be bespoke. See how things have transitioned from HTTP-polling, SSE, web sockets... etc, which is quite alot more varied compared to a standard "restful" API. |
Also one may note level 3 builds on top of level 2 and 1 mechanisms too. So it's important to understand when to use what. |
We will have both push and pull dataflows here in PK. But PKE might be the first place to start using it in-earnest before converting PK into it. |
If websockets conversations is possible MatrixAI/js-ws#2 is possible, then push flows from PK can push all the way pass the program boundary and send events out, by initiating calls to the client side. Doable as long as a connection already exists. If connections don't exist, then they can start a connection, but that requires that it's possible for the other side to run a server. |
Some notes from the experiments in PKE. Every domain object exposes 2 kinds of things:
The pull methods are any method call that returns a synchronous value or
All of the above are examples of "pull-methods". This is how we have structured all of our code currently in PK. The additional of reactive properties enables push-orientation that is "additive" to the architecture without breaking compatibility or changing the "pull-orientation". Reactive properties are observables, they may be nullary observables or parameterised observables. Although... I am not sure if parameterised observables make sense here.
You can see here, that it looks pretty similar to the pull methods, but the introduction of the Now other domain objects can by DIed with objects exposing reactive properties to produce their own reactive properties. When looking at this from the birds eye view, it produces a sort of push-pull architecture. This of course exists within a single program boundary. Inter-program boundary push-pull can only be done if the "network IO" supports it. Our RPC in js-rpc should enable this ability even in the situation where clients connect to servers, due the existence of RPC conversations. Another cool thing is that each object can maintain their own state, or reactive state. Then stateful objects are like lakes where reactive properties are like streams feeding into states. State inside each object is accumulated possibly using various collection structures. It does not impose anything on how to structure the state. Immutability is also not necessary, it is optional. However when binding to React components, immutability is necessary. Further exploration of this is PKE. Finally one must be clear that by default program architecture should be pull-oriented. It's the most natural way of programming. Push-architecture should only ever be introduced if you have the need to have either multiple origins of change, or multiple receivers of change. That's when push-architecture should be introduced. If you don't have this, there is no need for push-orientation. |
We have observed Observables to be the functional synthesis of all kinds of push-structures, whether they are callbacks, event emitters, pub/sub, streaming, observer pattern. I believe they are currently the SOTA in terms of push-orientation. RXJS is the most well maintained implementation of Any usage of Or... if you prefer to focus on observable-only libraries, one can supply plugins that wrap existing libraries to expose One day we may expect |
Interaction between pull and push.
|
Some ideas here that still need to be summarised. Best events architecture is being iterated on in js-quic, and will then be summarised for usage in other libraries and PK itself. Afterwards, it still makes sense to stick with |
Specification
The
EventEmitter
is a nodejs native construct. To be more compatible, we should be using Web APIs likeEventTarget
.We've started to do this in #438 (comment).
There are some differences:
Our own usage of
EventBus
is pretty simple. It does however add the ability to emit events asynchronously and return a promise to indicate when it has been received and executed. Making it closer to an observer pattern. I believe we will want to useEventBus
in a number of ways:Basically the event emitter already has the ability to both synchronous listeners and asynchronous listeners. The event bus just extended event emitter with the ability to emit asynchronously compared to emitting synchronously.
Once we change to using event target, the same exact usecases must still be available.
Still our primary usecase will be to wire up reactivity between the different domains in Polykey without hardcoding them. This represents a push-based API.
Additional context
NodeID
has changed. #386 - dealing with root key pair changesTasks
js-events
to introduce aEvented
decorator that can be used to augment classes withEventTarget
functionality, andAbstractEvent
to allow a common foundation for our typed events.js-events
intojs-async-init
, so that all decorated async-init now automatically supports events and properly dispatchesStart
,Started
,Stop
,Stopped
,Destroy
,Destroyed
events in relation to the lifecycle of domain objects.events.ts
file that lists all the typed events withEventX
. Such event classes should extend theAbstractEvent
.Observable
can then build on top ofEvented
decorator, and instead be application specific, rather than exposing them across all of the libraries. AnObserved
decorator could apply to existingEvented
decorators wrapping around all of theaddEventListener
anddispatchEvent
methods... or expose property or method decorators that do similar.The text was updated successfully, but these errors were encountered: