Giter Club home page Giter Club logo

cycle-restart's Introduction

cycle-restart

Hot module reloading is super cool. You can change your code in an instant, and you don't have to reload the page to see the result.

The most annoying part about it is that it throws away your application state! So if you want to develop with your app in a certain state, you have to recreate that state manually each time the code is reloaded.

cycle-restart solves that problem for you! It records all the actions you perform and replays them after you change your code. Best part is that it happens in the blink of an eye!

edge/cyc is a great boilerplate project to help you get started with cycle-restart.

Installation

$ npm install cycle-restart --save-dev

How do I use it?

cycle-restart is designed to be used with hot module reloading, provided either by browserify-hmr or Webpack.

You'll want to set up your entry point (usually index.js) like so:

import {setup} from '@cycle/run';
import {makeDOMDriver} from '@cycle/dom';
import {makeHTTPDriver} from '@cycle/http';

import {rerunner, restartable} from 'cycle-restart';

import app from './src/app';

const makeDrivers = () => ({
  DOM: restartable(makeDOMDriver('.app'), {pauseSinksWhileReplaying: false}),
  HTTP: restartable(makeHTTPDriver())
});

let rerun = rerunner(setup, makeDrivers);
rerun(app);

if (module.hot) {
  module.hot.accept('./src/app', () => {
    const newApp = require('./src/app').default;

    rerun(newApp);
  });
}

API

state refers to the object containing the sinks and sources returned by Cycle.setup.

restart(setup, main, drivers, state, isolate = {}, destinationTime = null) => newstate

Runs a new main and drivers, and replays any available history recorded on state. Drivers will only be replayed if they are restartable.

If you use isolate in your application, you should supply it so that its internal counter can be reset.

You can also supply a Javascript Date as destinationTime if you wish to replay actions only up to a certain time.

restartable(driver, pauseSinksWhileReplaying = true) => newDriver

Wraps driver with the ability to record a log of actions and restart. When restart is called, actions recorded on the driver's associated sources are replayed onto the driver.

If pauseSinksWhileReplaying is true, drivers will drop any items emitted during replay. Currently, this needs to be false for the DOM driver.

rerunner(setup, makeDrivers, isolate = {}) => rerun(main, destinationTime = null) => state

Takes Cycle.setup and a function that returns your drivers and produces a function that can be called with the same main and destinationTime as restart, but automatically passes isolate and the previous state.

The rerun pattern is an abstraction of the common use case where state is saved and passed to the next restart call, and isolate does not change. rerun can also be used instead of Cycle.run to start the application for the first time.

Browserify

cycle-restart works great with browserify-hmr.

Assuming we have an index.js with the above code, and src/app.js exporting our main function. We also need an index.html with a <script> that loads our bundle.js

First, we need to install watchify and browserify-hmr:

$ npm install watchify browserify-hmr babelify --save-dev

Then we can use watchify to serve our bundle:

$ watchify -t babelify -p browserify-hmr index.js -o bundle.js

You can also use budo as a development server. budo makes it easy to also live reload your css. For an example of this, see Widdershin/cycle-hot-reloading-example

Webpack

Have a look at the webpack docs for setting up hot module reloading.

The minimum requirement to get HMR working with webpack config is first to add these two entry points to your config.entry

entry: [
    // The script refreshing the browser on none hot updates
    'webpack-dev-server/client?http://localhost:8080',
    'webpack/hot/dev-server', // For hot style updates
    mainPath, // your actual entry
  ]

and of course, the HMR plugin itself in config.plugins

plugins: [new Webpack.HotModuleReplacementPlugin()]

Finally, run the command on the directory where your webpack config file is located. Defaults to webpack.config.js

webpack-dev-server --progress --colors --inline

For an example, look at https://github.com/FeliciousX/cyclejs-starter

SystemJS

(If anyone who has experience with SystemJS could add some instructions about how to get set up, it would be greatly appreciated)

Supported drivers

cycle-restart is tested against and known to work with the following drivers:

  • cycle-dom
  • cycle-http
  • cycle-jsonp
  • cycle-history
  • cycle-animation-driver

Other drivers are likely to work off the bat. If you encounter a problem, please raise an issue and we will try and add compatability. If you use a driver not on this list and it works, let us know about it.

Isolate?

cycle-restart does in fact support isolate. If you use isolate in your apps, simply pass it as an extra argument to restart.

  import {setup} from '@cycle/run';
  import {makeDOMDriver} from '@cycle/dom';
  import {makeHTTPDriver} from '@cycle/http';
+ import isolate from '@cycle/isolate';

  import {rerunner, restartable} from 'cycle-restart';

  import app from './src/app';

  const makeDrivers = () => ({
    DOM: restartable(makeDOMDriver('.app'), {pauseSinksWhileReplaying: false}),
    HTTP: restartable(makeHTTPDriver())
  });

- const rerun = rerunner(setup, makeDrivers);
+ const rerun = rerunner(setup, makeDrivers, isolate);
  rerun(app);

  if (module.hot) {
    module.hot.accept('./src/app', () => {
      const newApp = require('./src/app').default;
      rerun(newApp);
    });
  }

Caveats

cycle-restart relies on your main function being pure. That means all real world side effects need to be encapsulated in drivers.

Here are some of the things that are likely to break right now:

  • Accessing time without using a time driver such as cycle-animation-driver or cycle-time-driver. Time is a side effect. (This includes Rx.Observable.interval() and Rx.Observable.timestamp()).
  • Math.random(). If you're using random numbers in your program, you should use a generator that produces deterministic numbers such as the mersenne twister from random-js.

Contributing

Your contribution is extremely welcome. Please feel free to open issues, pull requests or help in any way you'd like. If you're unsure how best you can contribute, get in touch and we can chat.

License

cycle-restart is released under the MIT license. Please see the LICENSE file for full text.

cycle-restart's People

Contributors

feliciousx avatar jvanbruegge avatar ronag avatar staltz avatar widdershin avatar wyqydsyq avatar xtianjohns avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cycle-restart's Issues

Support applications with multiple streams

In my haste I took a shortcut that means if you call DOM.select(...).events(...) more than once it won't work! There's a failing test for this on the multiple-streams branch.

Restartable removes prototype functions from DOMSource

With the new Cycle diversity release candidates, the @cycle/dom driver source has a select function on its prototype, then I think the wrapSource function strips all the prototype functions out when it does this code:

Object.keys(source).forEach(function (key) {
  var value = source[key];

  if (key === 'dispose') {
    returnValue[key] = makeDispose({ streams: streams }, value);
  } else if (typeof value === 'function') {
    returnValue[key] = wrapSourceFunction({ streams: streams, addLogEntry: addLogEntry }, key, value, returnValue, scope);
  } else {
    returnValue[key] = value;
  }
});

As object.keys only loops over properties, not the prototype, it never adds the select function to the return value object.

I can put a pull request if you'd like to fix this. Using a for...in loop should do the trick.

Repeating when using cycle-restart?

Given the following code in an intent:

  const assetUpdate$ = id$
    .flatMapLatest(id => Observable
      .merge(
        DOM
          .select('.title')
          .events('input')
          .map((e) => ({
            id,
            title: e.target.value,
          }))
      )
    )
    .do(() => console.log(1))

When using cycle-restart:

1: id$ is fired once
2: press key in ".title" field
3: "1" is printed once
2: id$ is fired 4 times
2: press key in ".title" field
3: "1" is printed 5 times

What's going on?

Re-subscribing to the same DOM element seem to duplicate stuff?

Usage with systemjs-hot-reloader

systemjs-hot-reloader provides a __reload hook for each module, which can be used like so:

var state = [];
export function addItem(text) {
  state.push(text);
}
export function removeItem(text) {
  state = state.filter(item => item != text)
}
export function getItems() {
  return state;
}
window.addItem = addItem;
window.removeItem = removeItem;
window.getItems = getItems;

export function __reload(deletedModule){
  console.log('__reload');
  state = deletedModule.getItems();
  console.log('  restored items: ', state);
}

Example taken from alexisvincent/systemjs-hot-reloader#23 (comment)

Do you have any ideas how to make cycle-restart work given this model?

DOM driver not restarted correctly when using isolate

Just started using Cycle.JS and cycle-restart so the following may be just my ignorance :)

When using isolate in an app, no changes to the DOM code are loaded visibly by cycle-restart. If isolate is not used then the issue does not occur and updates to the code are reflected visibly after restart.

Digging into the code it seems that restartable explicitly does not wrap isolate methods (though I couldn't find the rationale for this in source / commit history).

Meanwhile, the isolateSource method on the DOM driver (v22.3.0) returns a brand new object. This new object is therefore never wrapped and its stream of events not recorded by cycle-restart for replay later.

Commenting out the logic in cycle-restart that excludes wrapping isolate functions solves the issue with no noticable side-effects:

// typeof name === 'string' && name.indexOf('isolate') !== -1 

Methods attached to stream like select() removed

//http like driver for graphql/apollo
    const response$$ = xs.merge(queryResponse$$, mutation$, subscription$)

    response$$.subscribe({})

    var select = (category) => {
      return category ?
        response$$.filter(responseFilter(category)) :
        response$$
    }

    var resulting = Object.assign(response$$, {select}, {client})
    return response$$

Only record history when using cycle-restart

Currently, the history api support I've added to cycle-dom will record all of your DOM events, which is a big obvious memory leak.

This should only occur when actually using cycle-restart.

Add support for cycle-animation-driver

Currently get this error when reloading:

Uncaught ObjectDisposedError: Object has been disposed
Disposable.checkDisposed @ rx.all.js:794
Rx.Subject.addProperties.onNext @ rx.all.js:11743
tick @ index.js:15659

TypeError: Cannot read property 'onNext' of undefined

I'm having trouble figuring out why this error occurs during hot-reloading of only DOM?

Any ideas?

Uncaught TypeError: Cannot read property 'onNext' of undefined(anonymous function) @ restartable.js:219
run @ rx.all.js:11214
ScheduledItem.invokeCore @ rx.all.js:980
ScheduledItem.invoke @ rx.all.js:968
Rx.VirtualTimeScheduler.VirtualTimeSchedulerPrototype.start @ rx.all.js:11124
(anonymous function) @ restart.js:64

DOM is alternatingly wiped and restored upon HMR updates

Going by my project here: https://github.com/wyqydsyq/unicycle

HMR itself seems to be configured fine, if I make changes to a file the HMR client will output that the relevant dependency graph has changed, the HMR update seems to contain the correct data, but when attempting to mount the new code it wipes the DOM underneath my app's mount point.

On the first update from HMR my app's DOM gets wiped completely (the node it was mounted to is left intact but has no children), on the second update I get an error in the console about Cannot read property 'removeChild' of null but my app's DOM gets correctly mounted based on the most recent update. It alternates between these two states.

Initial page load, everything is fine:
http://i.imgur.com/9oxdSdL.png

After first change (added an ! to the end of the text), DOM breaks:
http://i.imgur.com/JqsjyhA.png

After second change (added another ! to the end of the text), DOM is restored based on new code:
http://i.imgur.com/elYuxIF.png

This could very likely be just me doing something stupid, but this issue only surfaced when I updated Cycle and related packages to Unified versions and none of my config tinkering seems to have helped.

Nail down history API

Current API:

domDriver.replayHistory(history)
sources.history(): history

Current the format of history is:

{
  '.add': {
    '.click': {stream: new Rx.Subject(), events: [{timestamp, event}]}
  }
}

That data format is almost guaranteed to break when auto-scope lands, but I will cross that bridge when I come to it.

Fix cycle-dom test failure

should catch events from many elements using DOM.select().events() 
Error: Uncaught AssertionError: "First" === "Second" (http://localhost:7357/7934/test/browser/page/tests-bundle.js:1365)Error: Uncaught AssertionError: "First" === "Blecond" (tests-bundle.js:1365)
    at tests-bundle.js:18415:21
    at Function._throws (tests-bundle.js:454:5)
    at Function.assert.doesNotThrow (tests-bundle.js:485:11)
    at AnonymousObserver._onNext (tests-bundle.js:18414:16)
    at AnonymousObserver.Rx.AnonymousObserver.AnonymousObserver.next (tests-bundle.js:3075:12)
    at AnonymousObserver.Rx.internals.AbstractObserver.AbstractObserver.onNext (tests-bundle.js:3009:31)
    at AnonymousObserver.tryCatcher (tests-bundle.js:1351:31)
    at AutoDetachObserverPrototype.next (tests-bundle.js:12948:51)
    at AutoDetachObserver.Rx.internals.AbstractObserver.AbstractObserver.onNext (tests-bundle.js:3009:31)

module.hot.accept callback not invoked?

Following the example but using the following command line:

budo index.js:bundle.js -d public -- -t babelify -p browserify-hmr

"Reloading" is never printed:

if (module.hot) {
  module.hot.accept('./src/app', () => {
    console.log('Reloading')
    const app = require('./src/app').default
    restart(app, drivers, { sinks, sources }, isolate)
  })
}

However, in the browser log it prints:

[HMR] Updated modules ["src/components/hub/browser/asset.js", "src/components/hub/browser/grid.js", "src/components/hub/browser/index.js", "src/components/hub/index.js", "src/app.js"]

Any ideas what I might be doing wrong?

Performance Impact

Running cycle restart with a driver that gets by char update from text fields really slow things down.

Time?

Does operators like debounce, throttle and the like break hot reloading? These are kind of hidden side effects so I could understand if that is the case?

I'm asking because I've had an issue where I had to move a debounce into a driver in order to have hot-reloading working again. Though I'm unsure whether this is because of some other bug?

Support Cycle Unified

Currently cycle-restart is unpredictable when using Cycle Unified. After updating one of my projects where everything previously worked flawlessly to use Unified I noticed a few drivers not working as expected or not at all when being restarted. To be considered compatible with Cycle Unified, client-side standard drivers (DOM, HTTP, History) should all work out of the box (excluding wrapper configuration e.g. pauseSinksWhileReplaying). I'll be updating this task to track issues relevant to this as I encounter them.

  • #62 DOM is alternatingly wiped and restored upon HMR updates

rerun vs restart?

Noticed we now have both rerun and restart. What's the difference? When should on be used over the other?

Request for universal example

First, thanks for creating this.

I'm new to the Cycle ecosystem, and is finding it hard to piece together a working universal app with hot module replacement. It will be good to have an example of this.

Cannot read property 'Time' of undefined

Using cycle-restart with the latest @cycle/run (3.0.0) I get the following error upon initial load:

Uncaught TypeError: Cannot read property 'Time' of undefined
    at restart.js:154
    at restart.js:173
    at Object.options.path (index.ts:22)
    at __webpack_require__ (bootstrap 06d4da1…:659)
    at fn (bootstrap 06d4da1…:83)
    at Object.<anonymous> (process-update.js:132)
    at __webpack_require__ (bootstrap 06d4da1…:659)
    at bootstrap 06d4da1…:708
    at bootstrap 06d4da1…:708

Having a quick peek into the relevant source, I'm not sure how this code is even intended to work; the call to Cycle() returns the dispose method, but the code seems to be expecting an object with sources, sinks, and run properties:
image

As you can see in this screenshot from the debugger, when looping over the Time driver everything seems to be undefined:
image

Support for Cycle.js ^7.0.0?

Any news on supporting cycle-restart with the latest "diversity" iteration of Cycle.js? Perhaps with xstream? Thanks!

Restartable compromises symbol-observable interop

When iterating on source properties in restartable, symbol-observable support is compromised.

This means the following code won't work:

import { from } from 'my-stream-library';

export function component( sources ) {
  /* assuming that the DOM driver was wrapped */
  const source$ = sources.DOM.select( '#foo' ).events( 'click' );
  return {
    anything: from( source$ ), // this throws
  };
}

The error is specifically that source$ does not have a property from symbol-observable that is added by xstream for interop with different stream libraries.

I think this is related to #55, but is slightly different since it isn't about a problem with driver methods/props, but with source/sink props.

To test that this can work, just wrap a driver (DOM sources from @cycle/dom are an acceptable candidate), and test that the property exists.

import $$observable from 'symbol-observable';
export function component( sources ) {
  /* assuming that the DOM driver was wrapped */
  const source$ = sources.DOM.select( '#foo' ).events( 'click' );
  console.info( source$[ $$observable ] );
  /* ... */
}

Also, symbol-observable is just one case, this would be true of all properties that relied on symbols or supported custom features on streams. But it's just an example of the broader problem.

Happy to help work on this @Widdershin, and submit the PR, but it looks like you may have started some work already as part of the Cycle Unified milestone? Let me know how I can help, and I'll get on it.

Isolate support breaks in real world applications because of impurity

As seen in Widdershin/cycle-hot-reloading-example@b5c18b3

Here is a gif that demonstrates the problem

gif

This is because repeated calls to isolate without an explicit scope produce different results.

It seems like either we need to reset the value of isolate when we reload the code, or cycle-restart needs to account for the increase in values.

It seems tricky (with the current implementation of isolate) to reset it on code reload. @staltz @Frikki you folk got any ideas?

npm missing 'lib' directory (v.0.0.2)

I believe the npm package needs to have the built lib folder. Webpack is throwing Module not found errors when I try to import it.

Thanks!

I look forward to giving this a try with my example app. Have you tried this with @cycle/history yet?

Support simple drivers (observable in, observable out)

Funnily enough, I currently support drivers that have selector functions like @cycle/dom and drivers where the provided sources is an observable of observables (like @cycle/dom).

Some good candidates: @cycle/storage and cycle-animation-driver

Support read-only and write-only drivers

I'm all excited here putting hot reloading in a certain app. I noticed a bug because the restart code assumes every driver will return (sources), and that's not true. Some drivers only take in sinks (e.g. a console log driver) and some drivers only return sources (e.g. a read-only websocket driver). We need to support those, so we need to add some basic null/undefined check.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.