Skip to content
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

Major breaking change suggestion #17

Open
staltz opened this issue Sep 2, 2015 · 7 comments
Open

Major breaking change suggestion #17

staltz opened this issue Sep 2, 2015 · 7 comments

Comments

@staltz
Copy link
Member

staltz commented Sep 2, 2015

@Widdershin I think I just came up with a (hopefully) brilliant idea to make time travelling more automatic, with less customizations to do inside application code.

So far we have been tracking streams inside main. What if we would track only source streams, and all of them? By source I mean responses here:

function main(responses) {
  // ...
}

In other words, everything that drivers output. The time travelling panel would then show only these response observables, making it agnostic to main's internals. In a way the information it would display would be only "what went into the application". This will mostly represent the user's actions, but it can also represent HTTP responses, or anything coming from drivers.

The inspiration to this came when I was thinking about Flux/Redux's actions and how time travelling is all about rewinding/replaying actions. I started thinking about what actions represent, and in Cycle.js the main's semantics are clear: input are events from the outside world represented by drivers (user + server + etc) and output is the program's behavior. To implement global time travel we just need to replay driver responses.


On a more technical level, to do this, we would need to wrap drivers, for example:

Cycle.run(main, {
  DOM: wrapWithTimeTraveling(makeDOMDriver('.app')),
});

wrapWithTimeTraveling intercepts output Observables, and stores their events in a list. Then, to rewind, we need to somehow restart the program (maybe call Cycle.run again?) and make the wrapped output Observable emit events stored in the history. The reason why we need to restart the program is that this approach doesn't touch the app's internal state, so we need to reset it. Probably not super fast approach, but definitely solves the problem. The wrapped output Observable is probably most easily implemented internally as a subject, although there might be a nice way of making it just an Observable.

Then there is the problem of how to actually intercept the output Observable from drivers. If the driver is a simple function (input: Observable): Observable, then it's dumb easy. But the DOM driver isn't, it's function (vtree$: Observable): SelectableCollection, where SelectableCollection has .select(). Somehow wrapWithTimeTraveling would need to be aware that the DOM driver returns SelectableCollection and not a plain Observable, and then mimic that SelectableCollection.

On the other hand, we could think of pushing this responsability to drivers, so a driver should provide an implementation of its time traveled variant. This could be a convention for drivers. If they support it, then they would work, if they don't support it, then their outputs simply wouldn't appear in the time traveling panel and it wouldn't be used. This way we can mark some drivers as "Time Traveling Ready™".

Maybe one way of trivially doing this is giving an option to the driver:

var timeTraveler = renderTimeTravelingDebuggerAt('#container');

makeDOMDriver('#app', {timeTravel: timeTraveler});
// pretend that object isn't anymore for custom elements

I am pretty happy with this approach, also for testing and debugging. It's actually not hard at all to intercept a driver's output and log all events from its output observables to console.log or even some remote service like with websockets. In Flux world this is common to use actions as the full log of "what happened" in the app. In Cycle.js it would be similar, but the log has more low-level data, and doesn't leave anything out.

What do you think?

@TylorS
Copy link
Member

TylorS commented Sep 2, 2015

I like the idea of the driver holding the responsibility. Not all drivers are the same, so the driver itself would choose how to display their state. The driver would be able to provide further options that could display input and/or output or anything else that could be useful.

Using @staltz example

var timeTraveler = renderTimeTravelingDebuggerAt('#container');
                                                  // further configuration
makeDOMDriver('#app', {timeTravel: timeTraveler, input: true, output: false});
// pretend that object isn't anymore for custom elements

I'm very interested in seeing how this plays out.

@HighOnDrive
Copy link

It would also be useful to use time-travel in an end app for reporting at different verbosity levels and for undo/redo management. Firebase timestamps all edits and this could also be done for edits on changes to models outside the Firebase cache. Then when rolling back to a action it's timestamp can be used to index changes made to any model and roll back the model as well.

@Widdershin
Copy link
Member

I really like this idea @staltz, and it would nicely address issue #14.

It would be quite nice to keep the ability to log out internal application state to the time travel bar, but there are ways that can be done. I might even extract a package just for doing that (the stream visualization aspect), and then include it in this package.

An option I was toying with after reading this issue this morning is making this change by asking users to replace Cycle.run with CycleTimeTravel.run or something along those lines. In combination with the driver support, that would allow us to recycle the app when a user changes data in the past.

@staltz
Copy link
Member Author

staltz commented Sep 3, 2015

👍 for CycleTimeTravel.run ! Hadn't thought about that

@Widdershin
Copy link
Member

Thinking more about how to go from here to there.

My goal for the new API will be that by default, the only thing a user has to change is the run function they call.

By tapping in to run, I should be able to achieve all of the aims of cycle-time-travel without requiring anywhere near as much change to app code.

This is my thinking right now. I'm willing to bet that not all of this is possible as I imagine it, but here's what I'll try.

  • Create an instance of the existing timeTravelBar
  • Provide time control for each driver's responses, tapping in to the to be written driver time travel interface
  • Re-run the application every time driver events are inserted into the past (or when needed)
  • Somehow inject a log function into the main method to display streams in development
  • Combine the vtree$ of the timeTravelBar with that of the app's DOM

@staltz
Copy link
Member Author

staltz commented Sep 3, 2015

Combine the vtree$ of the timeTravelBar with that of the app's DOM

You don't actually need to do this part, you can render the time travel vtree$ in a newly created div e.g. document.createElement().

And since we're talking about this, how about investigating how to render that in a dev tools tab, and not in <body>?

@Widdershin
Copy link
Member

Progress update: https://gfycat.com/NextThirstyGermanspaniel

https://github.com/cyclejs/cycle-time-travel/pull/24/files #24
cyclejs/dom@master...Widdershin:time-travel-support

So now all you have to do to use cycle-time-travel is replace Cycle.run with TimeTravel.run. I've added a method to the dom driver, enableTimeTravel(time$). I doubt that's the final api we'll use, I'm still just hacking stuff together.

It figures out the streams to display by walking the observable tree. It's currently just using the old record/filter approach for time travel, which totally doesn't work for stubbing out responses.

The time travelling doesn't work right now. As you can see in the above .gif, it will replay old user intent when you rewind, which causes it to act like it's traveling forward in time.

The next step as far as I can see is to restart the Cycle app every time the user rewinds. I can just call Cycle.run again but that clears the user's intent. I think I need to record the user's intent, and then pass it into the driver along with enableTimeTravel (probably using a HistoricalScheduler). If there's an easier way to do that, I would love to hear it.

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

No branches or pull requests

4 participants