@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?