-
-
Notifications
You must be signed in to change notification settings - Fork 422
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
The next evolution of Cycle.js -- Cycle.js Neo #929
base: master
Are you sure you want to change the base?
Conversation
I appreciate your work! This change formalizes an existing common usage — wrapping main. One instance is @cycle/state. Another example is logging HTTP requests. Is this correct, please? |
Yes, but it also changes main wrappers as they don't wrap main directly any more as is the case today. first main is wrapped with the APIs to interpret the XXXSource calls into a plain stream that your wrapper can then inspect |
I really hope there will be an official |
@teohanhui why is this a dealbreaker? Why does the implementation matter? |
Because |
|
I've create a kanban board to track the progress here: https://github.com/cyclejs/cyclejs/projects/5 |
With this commit we can run a first hello world application with the HTTP driver! See https://twitter.com/jvanbruegge/status/1252547569617600513
There are now three tests (all related to race conditions) failing because I had to remove the buffering of responses in the driver as this would result in the application getting responses from the past (see the last test in browser.ts)
And another step is done: Isolation! |
@jvanbruegge how is it going with neo? I've got a question.
Who is responsible for handling/creating subscriptions and how they will be handled by the driver? If it is the API who create the subscription, so it is probably not so pure? |
@whitecolor I am currently working on The driver interface forces a driver to return a subscription, ie the driver has to subscribe (for a writeable driver). The subscription itself is then managed by export interface Driver<Source, Sink> {
provideSource?(): Producer<Source>;
consumeSink?(sink: Producer<Sink>): Dispose;
cleanup?(): void;
} |
And a new milestone is done: |
husky had a new major version and breaking changes with regard to where the hooks are saved.
Commitizen does not have any possibility to lint commit messages. In addition it is quite complicated to configure. Commithelper fixed both points
This automates publishing to npm, creating a release commit, tagging it, creating a release on github and generating the correct changelog.
npm exec was only added with npm v7 so the commit hooks will fail for people with older versions, preventing them from contributing
Great work @jvanbruegge. After reading the post on DEV I'm impatient tro see it ready for testing. |
I don't really see what Cycle would gain from the ability to dynamically add drivers. And even if there is a use case it will be very minor. The way cycle Neo structures messages (everything is serializable and wrappers have full access to the raw messages), it would be possible to build something like that yourself. A higher order stream would make it impossible to use drivers over APIs like postMessage. |
Hi all, this is the branch where all the future development of Cycle.js will happen. It is a big rewrite of the core and all drivers, but there are no changes to the overall philosophy of Cycle and the user facing changes will be minimal. On the other hand, the new design cleanly separates drivers out even further, so they are really just interpreting streams of commands and return streams of data. This means that more code can be used directly in tests and less code that still needs mocking.
The design goals
As already discussed in #760, we figured out that splitting drivers even further into a driver part that only works on streams and a wrapper part that creates a nice API (like the DOMSource for example) would be beneficial. The idea was to handle every effect separated from each other:
This has one severe limitation though: Unlike today, it is not possible any more to inspect and modify streams that your app emits in a normal main wrapper. I for example frequently use this functionality to add a header with a token to all outgoing requests where the token in question gets requested at app startup and then stored in the state.
On the other hand, question and answer side effects like HTTP is a bit awkward at the moment. FRP (being based on stream) is a really nice fit for user interfaces, so the DOM driver for example, but it gets tedious with stuff like HTTP.
Take this simple component for example, it is quite hard to read because instead of having a linear flow of information, we start with the response, wondering where the request came from, have to manually correlate response with request by using
category
and basically the whole right hand side ofres$
is boilerplate`.With the new design, we wanted to turn this component into something similar to this:
And while it is totally clear what it does and is nice to read, we have the first problem again: Now that we don't have any request that comes out of the sink, how can I intercept those requests in user code and modify them to my liking?
To find a solution for this, @staltz and me had a two hour long video call where with the help of a whiteboard and throwing ideas to each other we finally found a solution that solves the problem in an elegant way and allows not only to intercept HTTP requests, but any action to any driver! This means you can for example log the attachment of event listeners even though there won't be actual event listeners on the DOM because of how Cycle simulates event bubbling on its own (if this interests you, you can read my article about it).
The final design
The final design is a bit more complex from the general flow of information, but
@cycle/run
takes care of connecting all the different components together. With Cycle.js Neorun
will connect three different core components together instead of just two as currently. At the moment,run
just connects all the sinks of your application with the input of the drivers and gives whatever the drivers return back to the application as sources. This means if the driver returns a stream, the source the user sees will be a stream and if the driver returns an object like theDOMSource
, the user will see that.Cycle.js Neo splits its drivers into two seperate pieces, the driver that just operates on an input and output stream and the API that takes that low level stream and offers a high level interface to the user that in turn emits these low level events. So to come back to the example from earlier,
sources.HTTP.get
will send a request to the driver, filter out the response that matches the request and returs this response stream for the user to consume directly.So the only thing missing now is being able to intercept and modify those low level streams from user code. For this purpose and different than the design in #760, we have added a new part. The master wrapper (name most likely subject to change). We can think of the API parts of the drivers as fascades, they present a nice interface to the user, but on the other side they expect a low level stream as input and return a low level stream as output. This is done on each channel (ie HTTP, DOM, whatever) seperately. Seen together with your application, after all APIs are applied, we basically have a new main function but instead of expecting e.g. a
DOMSource
as input this new master main just expects the low level streams and also only returns those. The master wrappers then can wrap this new master main. This means that the master wrappers have access to all the events and commands from all drivers and can do whatever they want with them.This is also where
@cycle/state
will be implemented. In #760,state
was supposed to be an API without driver, but this would make having access to the state in the master wrappers impossible. But we did not want to make this a special case forstate
only, so we defined specific purposes to each of the parts:state
ori18n
should be implemented as a master wrapper.main
function that is already wrapped by all its APIs. It is still a pure fuction, but only accepts streams and only returns streams.@cycle/run
applies master wrappers in order, as inner wrappers can access potential new pure APIs that where created by the master wrapper (@cycle/state
will provide aStateSource
for example). The fully wrapped master main is then hooked up to the drivers. A master wrapper is a function that takes a master main and returns a new master mainA fully wrapped and connected main function may look like this then:
To connect everything together,
@cycle/run
does roughly these steps:PAQ (potentially asked questions)
How does multiple stream library support work with this new version?
The standard
@cycle/xx
packages like@cycle/http
will provide a driver that uses Callbags as streaming library because of its small size and it being only based on callbacks (ie no common core you need to always ship, it's just some callbacks). It will also provide aHttpAPI
that returns Callbags from all methods and also returns Callbags to the driver. You can use this API directly if you want to write your application with Callbags as streaming library.For those that prefer rxjs, most or xstream, there will be packages like
@cycle-rxjs/dom
or@cycle-xstream/dom
that just wrap those Callbag APIs and provide an rxjs, most or xstream interface. We have not fully decided if we want to have those packages as official cycle packages or better have them community maintained. But those packages will be very simple as they just convert from callbag to another stream lib. This also solves the Typescript issues we have currently for people that use e.g. rxjs, because@cycle/run
won't implicitly convert or adapt the streams any more. Everything is explicit.Is it now easier to run a Cycle.js app in a web worker?
cc @aronallen
Yes, because
@cycle/run
will expose a method that just connectsmain
with itsAPIs
returning the master main. This master main can then be wrapped with the master wrappers as needed. On the outmost layer you can have a master wrapper that takes the low level streams, serializes them and usespostMessage
to send them over to the main thread. On the main thread, you basically only have a master wrapper that takes all the low level streams and deserializes them.When will this be done?
We don't know. We have started the efford already. I've created
@cycle/callbags
, that will be the basis of the new implementation. I've also started working on a first version of the HTTP driver to try things out. To further downsize a typical Cycle.js app, I've created minireq, a request library that will be the base of the new HTTP driverWe have opened this PR so that everyone can see that we are actively working on it and to allow others to give their feedback to the new design. We will open more, smaller PRs that target this branch where all the drivers and other components will be implemented.
How can we help?
There are several things that you can do. First and foremost, give feedback to our ideas. We think the current design is pretty great, but we might have missed something! So please feel free to comment here. The second thing is supporting us through our Open Collective. Until now, we have not taken any money regarding the rewrite (the two hour design meeting excluded), but justifying spending a lot of time on Open Source work is a lot easier with financial support. We both love what we do, but we both have to live off something :)