Giter VIP home page Giter VIP logo

hotswap's Introduction

hotswap

Code hotswapping for Haxe generated JavaScript.

Disclaimer

Hot swapping code is a complicated matter. It is fair to say that in general, it is strictly impossible. It can only be made to work in specific cases. So even when this library has made it past its experimental stages, it will impose quite significant restrictions to the code it's supposed to work with.

It will also always have performance implications.

Inner workings

hx Namespace

When compiled with hotswap all classes compiled with -D js-unflatten and their names changed to hx.the$full$dotpath$ClassName.

This means that all classes wind up in a variable called hx, roughly like so:

var hx = {};
hx.Reflect = function() { };
hx.haxe$rtti$Meta = function() { };

The code swapping is primarily accomplished by assigning the value of hx from the new code to the hx variable present in the old code.

This has implications for some reflection APIs like Type.resolveClass and Type.getClassName. Their implementations are rewired to hide this difference. You may still notice it when looking at the generated JavaScript.

Initialization

When your code executes for the first time, hotswap does a few things:

  1. Closure wrapping: For any method closures discovered at compile time, the method is wrapped into an alias, roughly like so:

    var alias = 'hotswap.' + originalName;
    theClass.prototype[alias] = theClass.prototype[originalName];
    theClass.prototype[originalName] = function () { return this[alias].apply(this, arguments) };

    This is made so that $bind as used by Haxe to ensure method closures don't lose this doesn't copy the implementation in an unreversible manner.

  2. Prototype Indirection: Prepend an empty {} into the prototype chain of every class. This is so that during reloading Object.setPrototypeOf(oldClass.prototype, newClass.prototype) will affect all existing instances of oldClass.

  3. further static initialization (as genereated by Haxe).

  4. onHotswapLoad Callbacks: Any class that defines static function onHotswapLoad(firstTime:Bool) will have it invoked with a flag indicating whether the class is loaded for the first time (so during initialization it is always true).

  5. main entry point is called.

Reload

The relevant entrypoint for reloading is the following:

package hotswap;

class Runtime {
  static public function createPatch(source:String):Outcome<{ function apply():Bool; }, Dynamic>;
}

If parsing the source fails, you get a corresponding Failure, otherwise you get a patch, which you can apply to swap in the new code (returning false if you're trying to apply the same patch twice). Patching does a whole number of things. Note that the main entry point of the new code is not called. On nodejs, the current file is automatically watched and reloaded.

  1. the code is evaled (unless it's the same as the last value passed to hotswap.Runtime.patch), in a context where a var hxPatch = null is available and assigns the value of its hx "namespace" is assigned to hxPatch.

  2. by virtue of being loaded, the loaded code will perform closure wrapping, prototype indirection and its own static initialization

  3. any old class that defines static function onHotswapUnload(stillExists:Bool) will have it invoked with with a flag indicating if the class exists in the new code as well.

  4. the outer code keeps a reference to the previous value of hx and assigns the value stored in hxPatch to it. From this point forward, all code points to the new classes.

  5. the empty prototypes of the old classes are pointed to the protypes of the new classes.

  6. any writable static var or static dynamic function in the new classes is assigned the value from the corresponding old class. The basic assumption here is that these values are meant to change at runtime and therefore the current runtime value trumps the initial value from the new code.

  7. onHotswapLoad callbacks are executed, but the firstTime flag is now false for every class that existed in the previous code as well.

  8. hotswap.Runtime defines a static public var onReload(get, never):Signal<{ final revision:Int; }> that fires accordingly.

Some of the things that will not work

Many things will simply not work as might be expected, or at least desired, but let's list a few.

Keeping references to class objects

Upon reload, they will point to the old classes. Meaning that even Std.is(someFoo, staleReferenceToFoo) will yield false. Be inventive. Instead of passing around class references, pass around predicates, e.g.:

// Don't
  var type:Class<Dynamic> = Foo;
  // and later
  if (Std.is(candidate, type)) {
    trace('oh yeah!')
  }
// Do
  var typeChecker:Dynamic->Bool = v -> Std.is(v, Foo);
  // and later
  if (typeChecker(candidate))) {
    trace('oh yeah!')
  }

The second approach is also more flexible, because in essence it uses filter functions and those are composeable.

Having code in persisted anonymous functions

Upon reload, such code will not change, e.g.

document.addEventListener('click', function () {
  trace('clicked');
});

If you change that anonymous function above and the code is reloaded, the change is not reflected (unless of course the document.addEventListener section is reexecuted ... in that case make sure to cleanup event listeners in onHotswapUnload)

Removing classes or methods that are otherwise persisted

Let's consider this example:

class Foo {
  static function onHotswapLoad(firstTime:Bool)
    if (firstTime)
      document.addEventListener('click', function () {
        rejoice();
      });

  static function rejoice()
    trace('oh yeah, I just got clicked!!!!');
}

After the class is unloaded, the next click will lead to an attempt to call hx.Foo.rejoice() and that'll throw Cannot read property 'rejoice' of undefined.

Adding new fields that are initialized via constructor

class Foo {
  public function new() {
    document.addEventListener('click', handleClick);
  }
  function handleClick() {
    trace('click');
  }
}

Now let's suppose we changed it like so:

class Foo {
  var clicks = [];
  public function new() {
    document.addEventListener('click', handleClick);
  }
  function handleClick(event) {
    trace('click number ${clicks.push(even)}');
  }
}

On the next click, this will fail, because clicks never gets initialized (haxe in fact moves the initialization to the constructor, but the constructor is not executed on existing classes).

The way to avoid this, is to make the initialization of clicks lazy:

class Foo {
  var clicks(get, null):Array<MouseEvent>;
  function get_clicks()
    return if (clicks == null) clicks = [] else clicks;
  // ...

There's potential for solving this implicitly via macros. Using @:build(hotswap.Macro.process()) will move initialization into such lazy getters.

Changing field types

// Before
class Foo {
  var clicks(get, null):Array<MouseEvent>;
  function get_clicks()
    return if (clicks == null) clicks = [] else clicks;
  public function new() {
    document.addEventListener('click', handleClick);
  }
  function handleClick(event) {
    trace('click number ${clicks.push(even)}');
  }
}
// After
class Foo {
  var clicks:Int;
  function get_clicks()
    return if (clicks == null) clicks = 0 else clicks;
  public function new() {
    document.addEventListener('click', handleClick);
  }
  function handleClick(event)
    trace('click number ${++clicks}');
}

This is likely to lead to an array being incremented. Everything is possible in JavaScript, but the results are often undesireable.

Static Initialization

Be very careful around it. Avoid __init__ like the plague too. Both are a great way to shoot yourself in the foot, because they may just not quite work as expected during reload. When in doubt:

  • use lazy initialization
  • move it into onHotswapLoad, as that gives you more context and therefore more control.

hotswap's People

Watchers

 avatar

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.