jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.29k stars 455 forks source link

Webpack Third Party Issues / ES6 rewrite #196

Closed larrybotha closed 7 years ago

larrybotha commented 8 years ago

Been battling for a couple hours with compiling using Webpack, which is related to https://github.com/jonobr1/two.js/issues/171.

The built file throws errors on the third party modules, the biggest problem being that they are defined on this for each module, which Webpack won't allow you to write properties to. Because Webpack doesn't preserve the names of imports, importing the modules individually doesn't resolve the issue either, as the built two.js expects specific names. Importing two.js, two.clean.js, or two.min.js all throw the same error.

Converting two.js to use ES6 / UMD will make it universally accessible to different browser environments, but I don't see me being able to do anything tangible for the next few months. What I can do for now is point to strategies for doing this effectively.

Inspiration

React Router is an excellent source for inspiration on how to build a project for multiple environments. It's a good idea to npm install React Router and take a look at the built files in the es6, lib, and umd folders to see the results of how files are built for the different environments.

  1. All modules are imported through index.js
  2. External dependencies are imported as peer dependencies to prevent having to manage versioning and updating of those deps as they grow in their own projects: package.json | webpack
  3. Building runs 3 npm scripts concurrently, for CommonJS, ES6, and UMD
  4. Each npm script is responsible for building to its different environment, including minification, using either Babel (ES6 and CommonJS), or Webpack (UMD).
  5. A release script is manually run after updates to automate versioning, tagging, pushing, and publishing to NPM

The workflow is pretty extensive, but once setup (or pretty much copied) from React Router's, everything will be pretty automatic. The bulk of the work will come down to refactoring modules to be written in ES6 syntax, which will mostly come down to importing deps at the top of the files, and removing the IIFE wrapping those deps.

For building to CommonJS and UMD, this is an excellent article: It's Not Hard: Making Your Library Support AMD and CommonJS. UMD ensures that a full build is available as a browser global, CommonJS module, or AMD module - Webpack does this for us, though.

Temporary Solution

As a side note, a quick solution to importing two.js in a module with Webpack follows, though it requires a custom build of the script (and some testing).

// my-module.js
import Two from 'two.js';
...
// custom two.js/build/two.js
window._ = require('../third-party/underscore');
window.requestAnimationFrame = require('../third-party/requestAnimationFrame.js');
window.Backbone = {};
window.Backbone.Events = require('../third-party/events.js');
...
// webpack config

...
  module: {
    loaders: [
      ...
      // 'this' is undefined in 'strict' mode when building with babel loader.
      // let's ensure 'this' references the window object, required for this._, 
      // this.Backbone etc. in two.js
      {
        test: require.resolve('my-custom-two.js'),
        loader: 'imports?this=>window',
      },
      ...
    ],
  },
...

I'm a little baffled as to why no one else has brought up any problems with imports and Webpack, so if anyone can provide me with a working example it'd be much appreciated.


EDIT: Monkey patched a Webpack-friendly 'build' based on the clean build here: https://github.com/fixate/two.js/blob/webpack-monkeypatch/build/two.svg.webpack.js Contains only the svg renderer.

This monkey patched file can be imported straight into a module without any Webpack configuration, and removes _ and Backbone from the global space.

jonobr1 commented 8 years ago

Hey thanks for posting this!

Sorry about the delay on this... Have you tried the latest dev branch? It doesn't have any third-party dependencies. Does that branch work?

larrybotha commented 8 years ago

Haven't tried it yet, but I'll give it a bash!

Is dev stable enough for production?

jonobr1 commented 8 years ago

Should be. Passes all tests and is the same API. Should be noted that underscore and backbone don't exist in dev now. So if your app uses those you need to import them yourself. On Tue, Sep 27, 2016 at 12:03 AM Larry Botha notifications@github.com wrote:

Haven't tried it yet, but I'll give it a bash!

Is dev stable enough for production?

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/jonobr1/two.js/issues/196#issuecomment-249783038, or mute the thread https://github.com/notifications/unsubscribe-auth/AANbgaT7PdJZH-PUD5_tE9WMdOEuvEWyks5quL-_gaJpZM4KGCDd .

http://jonobr1.com/

larrybotha commented 8 years ago

Alright, gave the dev branch a bash and it's working but with the condition that Webpack's imports-loader is configured to inject window as this.

// webpack config
...
  module: {
    loaders: [
      ...
      {
        test: require.resolve('two.js'),
        loader: 'imports?this=>window',
      },
      ...
    ],
  },
...

This is because of defining Two on the global object, as opposed to allowing the UMD pattern to handle where and how Two is defined.

Thanks @jonobr1!

How do you feel about exploring an ES6 rewrite? I'd love to get on this when some time opens up, and it'll make for a much simpler dev process - no Grunt, UMD by default, and even the option to allow devs to import only the modules they need:

// import only the specific modules I need
import Two from 'two.js/src/two.js'; // core
import Path from 'two.js/src/path.js';

const two = new Two({...});
const path = new Path(...);

two.add(path).update();
jonobr1 commented 8 years ago

woah that would be rad! But, import Two from 'two.js/src/two.js'; doesn't look like ES5 compatible code..?

What do you use instead? Webpack? BTW, I never used grunt I keep things very simple with the node ./utils/build that just concatenates and compresses the files.

I can re-write aspects of Two.js that depend on window to only work if window exists. I believe it's just the fullscreen functionality, so it should be easy to remove. Thanks for trying this out!

larrybotha commented 8 years ago

Ye, I'm building with Webpack to build to ES5 compatible code.

If everything was written using ES6 modules, users writing in ES6 would be able to import like in the example above, while users needing to import the entire library would be able to use a script tag as per normal, because Webpack would be configured to build a UMD version of the full library :)

Ah ok, so the Grunt stuff is legacy?

widged commented 7 years ago

Help might be on the way. Early days of a port to es6 at: https://github.com/widged/two.js/tree/dev. You can turn it into good old es5 code with babel src --out-dir src (see https://babeljs.io/docs/usage/cli/). The dev branch was a big improvement, making it easier to work in a nodejs environment... but there were a few things that didn't work quite as expected (mostly interpret; more about that later), so I started trying to understand the code... and es6 really makes it easier to understand dependencies and data flow. Warning, I made some breaking changes when moving things around. Already changed to es6 modules. Now working on refactoring to es6 classes.

jonobr1 commented 7 years ago

Looking forward to checking out! What are the breaking changes @widged?

larrybotha commented 7 years ago

@widged before going down the ES6 classes route, take a look at what Kyle Simpson has to say about them: https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/apA.md

That whole series will change the way you think about JS.

You'll find Dan Abramov, Eric Elliot, Brian Lonsdorf, and a bunch of other influential devs who prefer to avoid use of the class syntax in favour of factories and composition.

Some food for thought :)

widged commented 7 years ago

@larrybotha Some people are allergic to classes because they are favoured by object-oriented languages. In js, classes are a way to define object factories. Nothing more. The reason to use them is that they provide much better code legibility, easier refactoring than good old prototypes. The reason to favour them is that they are a much more efficient than closures for writing object factories. Indeed, what is very rarely discussed is that with closures, a lot of memory gets wasted as any time a new closure is generated, the variables and functions listed in that closure are copied in the instance.

Dan Abramov, worth following every single one of his recommendations (which are principles of good software architecture).

Eric Elliot, worth reading for his interests for trait composition (my own exploration of it, a few years ago- http://www.slideshare.net/mlange/traits-composition)... but with caution.

The goal is to use classes to generate and manage state instances and little more, have most of the code written in a strict functional way (no this, no side effect, no mutation. However, this would require a lot more work to get there. Not that two.js is not well written. I found it to be superbly structured. But two.js relies heavily on underscore's helper functions and they don't actively discourage the use of this.

So, for now, I focus on porting to es6, much smaller modules, with clearly declared dependencies. Then I try and get rid of too tight couplings. One of the first changes I made was to rewrite all geometry files to return a list of points (framework agnostic)... and replace the various instances of makeCircle(...), makePolygon(...) with addShape(circle(...)) or addShape(polygon(...)) as it was critical for me to be able to load my own geometries as I was hoping to use two.js for webgl and canvas rendering for a polar charting library -- https://github.com/widged/react-polar-gg... but had some issues. Moving any two.js specific code outside of the geometry declaration also helps remove unncessary duplications. Compare https://github.com/jonobr1/two.js/blob/dev/src/two.js#L1289-1295 and https://github.com/jonobr1/two.js/blob/dev/src/shapes/ellipse.js#L14-20. For now, a breaking change, as I had mentioned. For pragmatic reasons only, as I don't have to worry about updating these functions when refactoring. It will be easy enough to restore the old public interface.

Then I will spend more time favouring composition over inheritance. For instance, for even management, I had to make classes inherit an EventEmitter because objects where written to have on/off/trigger directly assigned to them. I prefer them to have an emitter/dispatcher attribute (composition, not inheritance)... but this a change that is very difficult to carry on in a gradual way... so I need to leave it for later.

Then I will worry about broadening test cases (I only worry about basic drawing, all geometries/shapes, all renderers, linear and radial gradient, at the moment) and optimising performance.

I don't recommend spending too much time reviewing my code as it is just work in progress at the moment.... It will keep changing (a lot) for another few days. But if you do have the time for a peek... then I am happy to discuss specifics :-).

widged commented 7 years ago

@larrybotha Making good progress. A major improvement is a split of the old Two files into two classes.

TwoLight contains the absolute minimum to draw shapes on the screen and let users who like to compose, do so (opened to independent contributions of custom shapes and custom geometries). With Light, you write two.addGeometry(circle(...)) or two.add(interpretSvg(...)), which means that you can easily define and use a range of custom geometries or predefined shapes; people can write their own importers... what I was after.

TwoClassic is there to provide a public interface that supports all of the old functionalities. It has an animation player, which sends a tick event on each animation frame (all of the player related functionalities are isolated in a separate class, for reuse, through composition); it can act on the fullscreen option; and it provides a number of convenience functions to draw shapes (makeCircle, makeEllipse); and access to interpret and load. With classic, you write two.makeCircle(...).

And to reassure (previous comment), a lot of the functionality has been moved to arrow functions (no this available). Compare the original and the refactoring. The line count is down by 300 while I have added well over 100 lines of comments (copied from the website).

The last major change I want to make is replace the long list of flags and private accessors with a much simpler change tracker. That would reduce all shape file sizes signfiicantly and make it much easier for others to collaborate on the code.

larrybotha commented 7 years ago

@widged you're a machine! That's a great approach with the light vs classic, and it looks awesome for creating shape factories. Will make extending Two.js a cinch.

Exciting stuff!

widged commented 7 years ago

@larrybotha Much looser coupling. I removed all references to private shape properties from all renderers. Renderers don't have to know anything about the public methods of a shape class; they use a simple bridge. Change tracker in place. I have replaced shape._flag_A || shape._flag_B || shape._flag_C || shape._flag_D, with anyPropChanged(shape, ['A','B','C','D']);. No more need to write or generate getters and setters for all properties that renderers might need to access. Shape data are stored in a state attribute, the react way and these data, rather than the full Shape prototype, can be passed to the renderers (no actual dependency on React, only a simplistic implementation of getState, setState, beforePropertySet, afterPropertyChange). These changes might cause slower perf at loading time (hypothetical worst case scenario)... but it should make it much easier to optimise the code for better animation performance. That starts to be the end of the major structural changes I wanted to make. Not quite ready for use yet, as I haven't had a chance to test the SVG import and animation. But advanced enough for feedback.

jonobr1 commented 7 years ago

What does the es5 compiled code look like? And also what's the process of debugging when writing new features or trying to fix existing bugs?

I'm curious to see if these changes affect animation performance. I understand in theory it should speed things up but I'm not too familiar with how browser engines optimize es6 if they can and which ones can.

One reason the flags exist is because it limits function calls, object or array generation, and nested looping. This change back in v0.4.0, I think?, resulted in around an 8x boost in performance and 20x boost in performance on mobile.

I'm not super stringent about performance tuning the library (cause I don't think that's what the purpose of this library is for), but I am curious to see how your work compares!

Looking forward to digging in and nice work so far!

doug commented 7 years ago

+1 to es6, though might suggest rollup over webpack.

widged commented 7 years ago

Yes, that eternal trade-off between performance from a user point of view and ease of tracking down issues for a developer ;-). Personally, I am not interested in complex animations, I ended up making structural changes because I had too hard a time tracking down puzzling results (due to default centroid anchoring among other things). But, still, I tried to minimise compromising animation performance too much. In terms of performance, with anyPropChange(shp, ['A','B','C','D']), for the cost of one function call, you get the following benefits: (1) Isolating features means that it is easier to optimise them. Over time, new features are introduced; others are optimised. For instance, the new Array.includes is much faster than the old Array.indexOf. Flag values can be stored in a flat array, flag values can be stored as properties of an object... a renderer doesn't have to know the specifics. (2) By using anyPropChange(shp, ['A','B','C','D']) instead of shape._flag_A or shapeFlag.A, you are free to change the specifics anytime, without affecting any other part of your code, (3) It makes it much easier to run basic checks. Indeed, I have come across one _flag_mispeled which is a debugging nightmare with the old approach and straightforward to become aware off in the new one.

jonobr1 commented 7 years ago

Well, I look forward to learn more! Based on taking a peak at your repo I have no idea what's going on :)

widged commented 7 years ago

I know how it feels. Been feeling that way a lot too ;-). Now that changes will be less extensive, I will make the time to document. dev-electron is the electron app I use to run the code in development mode. What you need to run it is documented in the README. The folder es6 contains the source code. TwoClassic is the entry point to access the old public interface. It interacts with TwoScene (renamed from TwoLight). TwoScene takes a RendererDelegate as config parameter and creates a renderer instance linked to the scene. On update, the render method of that renderer is called. Any renderer must extend TwoRenderer which defines the interface that a scene expects from a renderer. To access shape data, a renderer uses renderer-bridge (I have just changed the name from shape-rendering). renderer-bridge provides methods such as anyPropChanged(shape, ['a','b','c']) or getShapeProps(shp, ['a','b','c']).

The rest is pretty much the same as it was before. What I did was to

As a convention, I start with an uppercase any file that has an object like structure or can be used to create objects. I start with a lowercase any file that only contains functions. I add 'fn' in the name when the file is meant to only contain pure functions (no side effects). Any 'fn' files should be associated with unit tests that provide 100% coverage.

widged commented 7 years ago

I had tried to keep the folder structure similar to the organisation of the original library. As it clearly didn't help, now completely reorganised. I am now done with changes and refactoring. Checking up animation, testing, and performance will be for next week-end.

For performance. It is really really weird that getting rid of a function call would cause such a massive improvement. It should not matter that much. Unless (1) a binding to this is created each time before the function is called. Very much a possibility with two.js given the systematic reliability on fn.apply or fn.call; or (2) there is a problem in the application logic that causes a recomputation to happen multiple times, even when no data have changed. Rapid trace checks suggest that this could well be the case as well in two.js.

In two.js, unnecessary reassignment / cloning is particularly costly because even listeners are attached to each path anchor (and most vectors). Events don't pass any data. They simply signal a change, which is then used to raise an anchors flag. An alternative could be to pass a change tracker to each anchor. Indeed, you don't need to know when something changes or what changed. You just need to know if anything changed. That requirement can be met by using a ChangeMonitor rather than an EvenDispatcher. Whenever an anchor changes, it calls monitor.change(), which sets the change value to true. All you have to do is check the tracker.change value before updating... example Anchor and checking whether a change happened before calling an expensive update function (not tested on animations, yet). A priori, optimisations of this type should lead to more performance improvement than removing a call to a pure function.

jonobr1 commented 7 years ago

I think you're conflating two notions of costly within the domain of performance. There is one type of costly which is the memory impact on a given application. This is what your stack overflow question refers to. Then there is the per animation tick (requestAnimationFrame) performance. Before the flags existed the bound events directly invoked changes in the renderer. It was kind of nifty, because you didn't have to iterate through the entire scenegraph every frame. However, it was incredibly slow because of how many function calls per animation frame. Instead of a linear slowdown based on how many objects were added to the scene it was an exponential.

Re:ChangeMonitor. It sounds like an interesting concept! But just to clarify, two.js does need to know when and what changed often. The when is the current frame, "Does something need updating?". The what is the flag. You'll notice the flags are used in if statements within the _update function. Recalculating all anchor's commands and control points is much more costly than simple changing the rotation for instance. And, for different renderers certain functionality is slower / faster because of how the underlying rendering system operates. This is one of the core reasons why I made Two.js. There is an abstract API to draw, animate, and keep track of a lot of things. Depending on your use case and the environment you're targeting svg or canvas might be better. Two.js makes this exploration a one-line change.

Looking forward to hearing your results!

widged commented 7 years ago

Two.js basic feature has high value. On the shape part, provide a way to convert shape geometries to a list of anchors akin the ones use in SVG. On the renderer part, provide a way to parse the anchors and plot them. That part is very solid. What I am trying to get at, in my comments, without being too obnoxious, is that the implementation of the mechanisms that define when a shape gets updated could be greatly optimised.

You said that you had decided to flatten your code, remove functions to optimise speed. The way flags are implemented, the way renderers access private methods from shapes, that was all to help optimise speed. However, you do mention exponential performance degradation and this suggests some flaw in the update logic, with either unnecessary recomputations and repaints.

Back to the trade off. (1) Micro-optimisations for application speed vs (2) refactoring to make it easier for the developer to improve the application logic. The stack overflow is about that trade-off. The overuse of fn.call and fn.apply instead of direct function callls could well increase memory usage, but this is very much that trade-off that I wanted to insist on. I wanted to address your mention that removing a small number (exact details left unspecified) of direct function calls did give you a very significant performance improvement (I presumed speed).

What the contributors to that Stack Overflow question argue is that you should first put your code in a state where you nail the logic... and only then, if you still need performance (speed) improvement, attempt micro-optimisations like removing function calls. What they argue is that you will typically see better improvements by fixing the logic than by rewriting a for loop into a while(i--) one (though, obviously, there are places where it is critical to write code in the most efficient way).

My own experience of porting two.js to es6 was that it was very difficult to become aware of the application logic (sequence of steps followed). A lot of code is repeated between renderers, but with weird variations (as if an optimisation that has been made in one has not transposed to the others). Calls to shp._update are made all over the place. In the current version of two.js, it is very difficult to become aware of an unncessary trigger of a recomputation of a shape's matrix data. And the numerous notes "TODO: Add a check here to only invoke update if need be." suggest that this something that you have been struggling with as well. Then, when running basic checks, I got evidence that when creating a WebGL scene, paths get recomputed more than once even though no path properties has changed. This would lead to the exponential (n^2) rather than linear (n) slowdown that you reported. Then it is easy to spot issues that are likely to cause much more performance degration than call to pure functions, like the event dispatcher attached to each anchor of a path. (Sorry to not have been specific enough. The ChangeMonitor was proposed as an alternative to even listeners for the anchors of a path. This is the place where you all you need to know is that at least one anchor changed. Flags -- through a ChangeTracker -- are kept for all other properties.

Hence the extensive refactorings. Much more than I had originally intended.

I do understand that I have made very many changes. This is daunting.

However, that refactoring opens up new opportunities. With the work I did, it is now possible to release, as @larrybotha suggested, two.js as three independent projects.

  1. one with the code to convert shapes to anchors;
  2. one with the code to render anchors to different surfaces;
  3. one providing a framework that can manage the transition from one to the other in a way that would notice changes to the shape properties and reflect these changes in the renderer's view.

I am interested in using (1) and (2) as part of a project that supports the plotting of visualisations on a diversity of surfaces. That context doesn't call for complex animations (interactivity would be more important than complex animations). I have limited motivation to spend much time working on (3).

So your current users will loose out.

All I heard so far is that you are not convinced that the type of improvements I have made can help you provide a better version of two.js.

Conversely, I am not in a position to help productively with the approach you prefer, flatten functions, make direct calls to what is defined (with the _private convention) as a private property of an object. That kind of approach makes it far too time consuming (and too stressful) to touch the code.

So, if we agree to disagree on the best way ahead, best thing for me is stop spamming your issues and provide access to projects 1 and 2 independently, with no concern to remain compatible with two.js

Take some time to think about it. If you need help setting the electron dev environment, let me know and I will make the time to help.

About bundling, the bundle files should really be treated as minified files. Not for humans to read or edit. Any transpiler will generate sourcemaps so that calls to console logs display the file and line number of the underlying es6 code instead of the one of the bundle file. For transpiling, you have various options, babel, rollup, typescript. They all have different strengths and weaknesses. Babel's strength is its popularity. Rollup has a tree-shaking step that ensures that the bundle only includes code that is actually used. Typescript's one is the clarity of the output (tsc can transpile code with no Typescript annotation). I don't really have a clear favourite. Presuming that you have babel-cli installed, you can run babel es6 --out-file bundle.js

jonobr1 commented 7 years ago

Thanks for taking the time on a thorough and thoughtful response! I apologize if I'm putting you off for doing the work you've already done. That's not my intention! I would like to increase the performance. However, not at the expense of maintaining the project. Now, as I understand it es6, babel, and electron are convenient for you to develop, which is great. I've heard a lot of great things about all of this. Unfortunately, I have never written es6, don't really know the environments you're talking about all on top of the rewrite of the project you've done. It's such a big update that I don't know the difference between a change that is really helpful and one that is merely your opinion / style. I actually don't even know how to run the development environment...

So, short answer is that I'm very interested about the work you've done. But, I need help understanding / using it. Please understand that when I started writing this (ca. 2012) the landscape of JavaScript development was pretty different and my knowledge of it was pretty basic. Honestly, my knowledge of it is still pretty basic. You've gathered that from the current source.

As someone who maintains this project in my spare time with collaborators who have come and gone and sometimes come back, the unfortunate reality is that at this point in time the project is only as good as my understanding of JavaScript.

I think a short chat if you could outline how you setup your development environment and explain a couple of the paradigms for organizing logic would help me wonders. If it's as easy as everyone says it is then it shouldn't take long, right?

I think after that and some time running some tests I could give you a definitive answer on whether we can merge this into the project or not or something to that effect. It looks like you've done some great work and you've made some really great arguments. But, the proof is in the code and I don't even know where to start with your branch. If you're up for it email me directly and we can set something up: removed email

widged commented 7 years ago

@jonobr1 sorry for the late reply. I live in NZ and we had a rather distracting earthquake (all is fine). As said, I made the changes initially for my own sake. Yes, es6, babel, webpack. A lot of changes to take on board for a project maintained in your spare time. And I fully understand all reasons you have to not make the jump (I never said you should). Babel, webpack, might be replaced with other tools within 2 years. ES6, however, is here to stay. And it really helps make a project the size of yours easier to maintain, in the longer term. Happy to talk you through. I will send you an email and we can arrange some time to talk on Skype :-). As said, I live in NZ, very different time zones.

jonobr1 commented 7 years ago

Not a problem @widged, I totally understand! Thanks for the reply! I look forward to receiving an email. I know the time difference is pretty rough, but even just text chat could do the trick. Happy Friday!

frederikbrinck commented 7 years ago

@larrybotha I'm running into the same issue trying to use Two.js with ReactJS. Larry, can you by any chance elaborate a bit on your temporary work around?

jonobr1 commented 7 years ago

@frederikbrinck, @larrybotha has made some examples of React + Two. Here's one of them: http://codepen.io/larrybotha/pen/qaRjJr

Also, it's recommended you use the dev branch if you're trying to pair with a library like React.

larrybotha commented 7 years ago

@frederikbrinck take a look at this comment - I'm using the dev branch.

There's some issue somewhere with our setup where some files that you would normally import Two.js into will throw errors on Webpack compilation because the module has been imported elsewhere. Bizarrely, not importing Two.js into those modules works and compiles without issue. It's difficult to diagnose because we're using npm linkd modules, so it may not even affect you.

jonobr1 commented 7 years ago

The dev branch now compiles with Webpack. When v0.7.0 hits stable the npm install two.js module should work correctly.

ddnn55 commented 6 years ago

I'm on "two.js": "^0.7.0-alpha.1" but I'm getting: image Do I need to use actual dev? Is it possible I'm seeing this issue intermittently (wtf)?

ddnn55 commented 6 years ago

It seems switching to dev did resolve the issue.

jonobr1 commented 6 years ago

Glad to hear that fixed it. v0.7.0 stable will be released soon!

On Fri, Mar 16, 2018, 5:53 AM David Stolarsky notifications@github.com wrote:

It seems switching to dev did resolve the issue.

— You are receiving this because you modified the open/close state.

Reply to this email directly, view it on GitHub https://github.com/jonobr1/two.js/issues/196#issuecomment-373704247, or mute the thread https://github.com/notifications/unsubscribe-auth/AANbgf_pVH_h2uAv1zbYfhXCy9hjMGhqks5te7W_gaJpZM4KGCDd .

-- http://jonobr1.com/