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 changes: eager updates, lazy QuerySets, internal refactor, more #62

Merged
merged 31 commits into from Jan 5, 2017

Conversation

Projects
None yet
6 participants
@tommikaikkonen
Owner

tommikaikkonen commented Nov 14, 2016

This will most likely be the next major release, 0.9 or 1.0. It has a number of breaking changes that greatly simplify the API and internals, and eliminates some significant problems users have: #60, enables fixing #50, having to tap into the internal updates queue to write application logic.

The breaking changes are quite big. However, the vastly simplified internals should make it far easier for other people to contribute to the code, and anyone to debug the problems they might experience using the library.

If you want to influence some of the changes outlined here, please comment in this PR.

The list below is prone to changes - this is still a work in progress. I've tested the new version by migrating redux-orm-primer to use the new version locally, and it's working well. For redux-orm-primer, the amount of steps needed to migrate were small. For someone who's used redux-orm internal APIs, it can take longer.

  • BREAKING: Eager updates

    All updates are now applied eagerly. getNextState is removed. The state property of a Session instance will always point to a state where all updates have been applied. An improved version of immutable-ops that utilizes batch tokens will make sure that the initial state will not be modified, and all further edits are applied with mutations.

    This eliminates problematic scenarios for users where they need to make decisions about further updates by looking at the database state with all previous updates applied, or need to reference objects created in the current session. Previously you would've had to call getNextState() and start a new session, which was unwieldy.

    All immutability is now abstracted away to the Database layer; Using the ORM is now like using any backend ORM that applies updates instantly.

  • BREAKING: Integration with Redux separated from the main API

    Previously the Session and Schema classes were needlessly coupled with Redux actions and reducers. Redux integration is now done with two functions: createSelector and createReducer which take a Schema instance as the first argument.

    Returning a custom new state from Model.reducer is not supported anymore. You can hack it together if really you need it, though.

    Changes to Schema interface:

    • Removed: reducer() (use separate createReducer function)
    • Changed: from(state, [action]) -> session(state) (clearer naming)
    • Changed: withMutations(state) -> mutableSession(state) (clearer naming)
    • Removed: createSelector() (use separate createSelector function)
    • Removed: define(). This was a shortcut to defining Models but I don't think many people were using it.

    Changes to Session interface:

    • Removed: getNextState() -> just access session.state.
    • Removed: reduce() -> use reducer returned by createReducer
    • Removed (internal): getUpdatesFor()
    • Removed (internal): updates attribute
    • Removed (internal): addUpdate() -> applyUpdate()
    • Removed (internal): getState
  • BREAKING: QuerySets are lazy

    With the eager updates, it makes sense to run QuerySet queries only once it has been constructed to it's final form.

  • BREAKING: withRefs/withModels QuerySet API removed

    With lazy QuerySets, this API doesn't make as much sense anymore. filter/exclude/orderBy are applied lazily, until the QuerySet is evaluated through toModelArray() or toRefArray(). After that, the user can use native functions to operate on the evaluated collection.

  • BREAKING: Backend class removed, database decoupled from ORM layer

    Backend has been transformed into Table that a new Database will utilize. Table will now also handle filtering/excluding/ordering with queries. The ORM layer does not need to know about Database implementation anymore.

    This clarifies the internals significantly.

  • INTERNAL: Transaction class removed

    With an improved version of immutable-ops that uses batch tokens to enable transient mutation, the Transaction class became redundant and was deleted. The database is still passed transaction info as an object, but that object doesn't maintain any of it's own state.

  • FEATURE: Custom Through Models

    This will include #39

  • FEATURE(todo): non-relationship attributes

    Not added yet, but the logic provided in #61 will be applied here as well.

Todo

  • Add in #61
  • Possibly move Model.nextID logic to Database, use new capability to attach meta information
  • Update Backend/Table options specification API
  • Possibly rename Schema.
  • Update docstrings (a lot of them we're left as is while this is a work in progress)
  • Update documentation
  • Write migration guide
@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Nov 14, 2016

Collaborator

Oh, sure. I go and write a couple blog posts about using Redux-ORM, and you decide to promptly invalidate half of what I've written? Last time I ever try to publicize anything YOU'VE developed!

:)

Okay, okay, more seriously. Quite a lot to absorb here. A couple quick initial thoughts:

  • Are there any major perf changes from applying updates eagerly? Any good perf benchmarks overall? Would be nice to see some before-and-after as a whole, both to see the differences as well as get an idea of estimated ops/sec in some different scenarios.
  • Eager updates would definitely make a number of scenarios easier to deal with. Out of curiosity, what happens in a case where, say, you delete a model instance, but still have the reference to the instance lying around?
  • Yeah, I'd noted that Redux-ORM could actually be used to work with any plain JS object that had been formatted right, not just if it was a slice of Redux state. Splitting out the Redux-specific parts could make it more of a general-purpose immutable data lib.

Also, I'd call it 0.9 and hold off on 1.0 until the new API is stabilized and any kinks worked out.

I'll have to check things out more in the next few days.

Collaborator

markerikson commented Nov 14, 2016

Oh, sure. I go and write a couple blog posts about using Redux-ORM, and you decide to promptly invalidate half of what I've written? Last time I ever try to publicize anything YOU'VE developed!

:)

Okay, okay, more seriously. Quite a lot to absorb here. A couple quick initial thoughts:

  • Are there any major perf changes from applying updates eagerly? Any good perf benchmarks overall? Would be nice to see some before-and-after as a whole, both to see the differences as well as get an idea of estimated ops/sec in some different scenarios.
  • Eager updates would definitely make a number of scenarios easier to deal with. Out of curiosity, what happens in a case where, say, you delete a model instance, but still have the reference to the instance lying around?
  • Yeah, I'd noted that Redux-ORM could actually be used to work with any plain JS object that had been formatted right, not just if it was a slice of Redux state. Splitting out the Redux-specific parts could make it more of a general-purpose immutable data lib.

Also, I'd call it 0.9 and hold off on 1.0 until the new API is stabilized and any kinks worked out.

I'll have to check things out more in the next few days.

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Nov 14, 2016

Owner

Hehe, sorry about that -- breaking backwards compatibility is really annoying, but once I started implementing the eager updates based on the problem in #50, it became apparent how it'd simplify a lot of things in the API, user experience and internals. I just had to roll with it :)

As to your comments:

No changes to performance unless people have been throwing away Sessions with unapplied, queued updates, in which case the cost of those updates would be incurred. My guess is that it's a rare use case.

To the deletion scenario, you would still be able to access the fields if you hold a reference, but doing a Model.hasId(deletedModelInstance.id) or any other queries seeking it right afterwards would not find it.

session.Book.withId(0).delete();
session.Book.hasId(0);
// redux-orm=0.8: true, redux-orm=0.9: false

Yeah, I'm not pushing the library outside the Redux community, but this makes it easy to use outside it anyway. More importantly I like the decreased cognitive overhead when working on the internals.

Good point about the version. It might make sense to publish out a release candidate to NPM for this before a proper version, since there's major changes.

Owner

tommikaikkonen commented Nov 14, 2016

Hehe, sorry about that -- breaking backwards compatibility is really annoying, but once I started implementing the eager updates based on the problem in #50, it became apparent how it'd simplify a lot of things in the API, user experience and internals. I just had to roll with it :)

As to your comments:

No changes to performance unless people have been throwing away Sessions with unapplied, queued updates, in which case the cost of those updates would be incurred. My guess is that it's a rare use case.

To the deletion scenario, you would still be able to access the fields if you hold a reference, but doing a Model.hasId(deletedModelInstance.id) or any other queries seeking it right afterwards would not find it.

session.Book.withId(0).delete();
session.Book.hasId(0);
// redux-orm=0.8: true, redux-orm=0.9: false

Yeah, I'm not pushing the library outside the Redux community, but this makes it easy to use outside it anyway. More importantly I like the decreased cognitive overhead when working on the internals.

Good point about the version. It might make sense to publish out a release candidate to NPM for this before a proper version, since there's major changes.

@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Nov 14, 2016

Collaborator

So lemme ask this: what, if anything, changes in the Model.reducer() case and the "create a Session manually in your own reducer" case? For example, if I currently end my own reducer functions with return session.reduce(), what would I do instead? return session.state ?

Collaborator

markerikson commented Nov 14, 2016

So lemme ask this: what, if anything, changes in the Model.reducer() case and the "create a Session manually in your own reducer" case? For example, if I currently end my own reducer functions with return session.reduce(), what would I do instead? return session.state ?

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Nov 14, 2016

Owner

Not much. Yep, you'd return session.state.

Previously:

function myReducer(state, action) {
    const session = schema.from(state);
    if (action.type === 'CREATE_BOOK') {
        session.Book.create(action.payload);
    }
    return session.reduce(); // or session.getNextState();
}

Now:

function myReducer(state, action) {
    // Changed naming: schema.session makes it clearer that we're creating a session.
    // schema.from naming implied we'd be creating a schema.
    const session = schema.session(state);

    if (action.type === 'CREATE_BOOK') {
        session.Book.create(action.payload);
        // Book.create above calls Session.prototype.applyUpdate
        //     -> calls Database.prototype.update which returns new state
        //     -> session.state is replaced with new state
    }

    return session.state;
}
Owner

tommikaikkonen commented Nov 14, 2016

Not much. Yep, you'd return session.state.

Previously:

function myReducer(state, action) {
    const session = schema.from(state);
    if (action.type === 'CREATE_BOOK') {
        session.Book.create(action.payload);
    }
    return session.reduce(); // or session.getNextState();
}

Now:

function myReducer(state, action) {
    // Changed naming: schema.session makes it clearer that we're creating a session.
    // schema.from naming implied we'd be creating a schema.
    const session = schema.session(state);

    if (action.type === 'CREATE_BOOK') {
        session.Book.create(action.payload);
        // Book.create above calls Session.prototype.applyUpdate
        //     -> calls Database.prototype.update which returns new state
        //     -> session.state is replaced with new state
    }

    return session.state;
}
@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Nov 14, 2016

Collaborator

Yeah, I can get behind that. Nice!

Collaborator

markerikson commented Nov 14, 2016

Yeah, I can get behind that. Nice!

Improve interface between database and ORM, move more logic to databa…
…se, rename Schema to ORM, clean out unneeded API.
@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Nov 20, 2016

Collaborator

Not sure I follow the reasoning behind renaming Schema to ORM. What's the thinking there?

Collaborator

markerikson commented Nov 20, 2016

Not sure I follow the reasoning behind renaming Schema to ORM. What's the thinking there?

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Nov 20, 2016

Owner

It's not really a schema. This became clearer as I separated the ORM layer and the database layer.

A schema in this context means a representation of the structure of the database. The ORM class, previously Schema, registers models and provides methods to start database sessions. Registering the Models makes a bit of sense, as it contains the database structure representation, but it also includes a lot more. But starting a database session from a schema doesn't make sense; how would you start a session from a description of a database structure?

The new ORM/Schema internal implementation generates a true schema (almost plain JSON-like object structure) based on those models and their field definitions, passes it to a createDatabase(schemaSpec) function that returns an object that conforms to the Database interface (implements getEmptyState, update and query methods). After the refactor, the database is not aware about Models, Session or Schema/ORM.

I thought about different names here but ended up with ORM - Object Relational Mapper. I think these sentences make sense:

Register a Model to a Object Relational Mapper
Given a database state, start a session from an Object Relational Mapper

I might add an alias here with the old Schema name that logs a deprecation warning, though.

Owner

tommikaikkonen commented Nov 20, 2016

It's not really a schema. This became clearer as I separated the ORM layer and the database layer.

A schema in this context means a representation of the structure of the database. The ORM class, previously Schema, registers models and provides methods to start database sessions. Registering the Models makes a bit of sense, as it contains the database structure representation, but it also includes a lot more. But starting a database session from a schema doesn't make sense; how would you start a session from a description of a database structure?

The new ORM/Schema internal implementation generates a true schema (almost plain JSON-like object structure) based on those models and their field definitions, passes it to a createDatabase(schemaSpec) function that returns an object that conforms to the Database interface (implements getEmptyState, update and query methods). After the refactor, the database is not aware about Models, Session or Schema/ORM.

I thought about different names here but ended up with ORM - Object Relational Mapper. I think these sentences make sense:

Register a Model to a Object Relational Mapper
Given a database state, start a session from an Object Relational Mapper

I might add an alias here with the old Schema name that logs a deprecation warning, though.

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Dec 12, 2016

Owner

The release candidate for this 0.9.0-rc.0 is now published on NPM

See the migration guide here
See the updated README.md here

Please give the new version a go, and leave any feedback here. If you see bugs please file an issue. If no problems come up within a week or so, I'll rerelease without the rc tag.

Owner

tommikaikkonen commented Dec 12, 2016

The release candidate for this 0.9.0-rc.0 is now published on NPM

See the migration guide here
See the updated README.md here

Please give the new version a go, and leave any feedback here. If you see bugs please file an issue. If no problems come up within a week or so, I'll rerelease without the rc tag.

@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Dec 12, 2016

Collaborator

One question that pops out: are attr() tags now required for every data field? Does that mean that it's basically a schema definition now? Does that eliminate the previous dynamic-ish-ness, where Redux-ORM didn't care what fields you actually had in your underling JS object, it just created properties for all of them automatically on instance creation?

Collaborator

markerikson commented Dec 12, 2016

One question that pops out: are attr() tags now required for every data field? Does that mean that it's basically a schema definition now? Does that eliminate the previous dynamic-ish-ness, where Redux-ORM didn't care what fields you actually had in your underling JS object, it just created properties for all of them automatically on instance creation?

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Dec 15, 2016

Owner

Yes, that's correct; all possible fields need to be declared. So if you're using the same model in a polymorphic fashion, you need to have it for every possible field. But for all logic, Redux-ORM ignores attr() definitions without any options passed, you can still have any kind of values in them. And yeah, it's basically a schema definition, albeit a very simple one.

While making things a bit more restrictive and verbose, this fixes some unexpected edge cases - e.g. without Proxies, Redux-ORM 0.8 could not define setters for fields unless the Model was instantiated with that field, e.g.:

const book = Book.create({ id: 0, title: 'Example Book' });
book.releaseYear = 2016; // doesn't work
book.set('releaseYear', 2016) // works, need to use this or `.update`

But

const book = Book.create({ id: 0, title: 'Example Book', releaseYear: undefined });
book.releaseYear = 2016; // works

With 0.9, setters always work for declared fields, even if the field key isn't defined in the underlying database object.

Owner

tommikaikkonen commented Dec 15, 2016

Yes, that's correct; all possible fields need to be declared. So if you're using the same model in a polymorphic fashion, you need to have it for every possible field. But for all logic, Redux-ORM ignores attr() definitions without any options passed, you can still have any kind of values in them. And yeah, it's basically a schema definition, albeit a very simple one.

While making things a bit more restrictive and verbose, this fixes some unexpected edge cases - e.g. without Proxies, Redux-ORM 0.8 could not define setters for fields unless the Model was instantiated with that field, e.g.:

const book = Book.create({ id: 0, title: 'Example Book' });
book.releaseYear = 2016; // doesn't work
book.set('releaseYear', 2016) // works, need to use this or `.update`

But

const book = Book.create({ id: 0, title: 'Example Book', releaseYear: undefined });
book.releaseYear = 2016; // works

With 0.9, setters always work for declared fields, even if the field key isn't defined in the underlying database object.

@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Dec 16, 2016

Collaborator

Yeah, I understand most of the intent here. I'm just trying to figure out if there's any relevant use cases that this change breaks.

Is there maybe some middle ground? Stuff works nicer with attr() declarations, but it would still create properties for any other fields that happen to be in the object?

Collaborator

markerikson commented Dec 16, 2016

Yeah, I understand most of the intent here. I'm just trying to figure out if there's any relevant use cases that this change breaks.

Is there maybe some middle ground? Stuff works nicer with attr() declarations, but it would still create properties for any other fields that happen to be in the object?

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Dec 16, 2016

Owner

What's the use case you're specifically worried about attr() affecting? Having a large set of non-relational fields on a Model that you don't touch but would have to declare?

What exactly we do with attr() can still be changed. The logic that uses attr() is limited to Model.js in methods _initFields() and create(), which you can take a look at. The current logic restricts possible passed values to the declared fields. I'm open to other ideas—ideally you'd be able to easily restrict the fields to declared ones if the base implementation is made more flexible or vice versa.

Owner

tommikaikkonen commented Dec 16, 2016

What's the use case you're specifically worried about attr() affecting? Having a large set of non-relational fields on a Model that you don't touch but would have to declare?

What exactly we do with attr() can still be changed. The logic that uses attr() is limited to Model.js in methods _initFields() and create(), which you can take a look at. The current logic restricts possible passed values to the declared fields. I'm open to other ideas—ideally you'd be able to easily restrict the fields to declared ones if the base implementation is made more flexible or vice versa.

@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Dec 16, 2016

Collaborator

Eh, combination of a few different things. It's easier to declare just the relations than to declare all the actual fields; could definitely be cases where the data is more dynamic and not always known ahead of time; there's overlap between declaring schemas and static typing things, and so on. Also just kinda hesitant to jump from a more "do whatever you want"-type setup to "you must declare all the things".

Half-formed concerns overall.

Collaborator

markerikson commented Dec 16, 2016

Eh, combination of a few different things. It's easier to declare just the relations than to declare all the actual fields; could definitely be cases where the data is more dynamic and not always known ahead of time; there's overlap between declaring schemas and static typing things, and so on. Also just kinda hesitant to jump from a more "do whatever you want"-type setup to "you must declare all the things".

Half-formed concerns overall.

@orecus

This comment has been minimized.

Show comment
Hide comment
@orecus

orecus Dec 17, 2016

Thanks for rc0! Just migrated my code and so far no real issues, the basics seems to work just fine! :) I will probably come with more specific feedback once I migrated the full codebase and tested it properly.

-- Snipped --

Update: I just noticed that you also updated the primer, my question was answered in there with the updates done to it. :)

orecus commented Dec 17, 2016

Thanks for rc0! Just migrated my code and so far no real issues, the basics seems to work just fine! :) I will probably come with more specific feedback once I migrated the full codebase and tested it properly.

-- Snipped --

Update: I just noticed that you also updated the primer, my question was answered in there with the updates done to it. :)

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Dec 19, 2016

Owner

New release candidate is 0.9.0-rc.1. That includes the fix for #29 ping @Aryk

The README.md and source for the release is in the branch for this PR. master and the docs on gh-pages are still 0.8, if you want to read the new API reference, you need to clone the repo then npm install and make build and open docs/index.html.

0.9 migration guide has been updated per rc1

I gave the fields issue some thought and decided to leave it more flexible for now - defining attr() for each field is not required but recommended. I moved the definition of getters and setters to the prototype for declared value fields, so model instantiation will be faster if you define the fields.

I chose this way because it's fairly easy to add safety provided by accepting only declared fields. Doing it the other way around would've required a more elaborate system (perhaps building in a validation system customizable by the user), and maintained the backwards incompatibility brought by attr.

If you want the safety of only accepting declared fields, you can use something like this snippet to declare your own base model (referring to issue #59, ping @pronebird). You could also delete undeclared fields with small modifcations before passing it to super.create/super.update

(not tested)

import { Model, QuerySet } from 'redux-orm';

function validateModelData(data, model) {
    const fieldNames = Object.keys(data);
    const declaredFields = model.fields;
    fieldNames.forEach(fieldName => {
        if (!declaredFields.hasOwnProperty(fieldName)) {
            throw new Error(`Got value for undeclared field ${fieldName}`);
        }
    });
}

class MyBaseModel extends Model {
    static create(props) {
        validateModelData(props, this);
        return super.create(props);
    }

    update(props) {
        validateModelData(props, this.getClass());
        return super.update(props);
    }

    // setters delegate to .set
    // .set delegates to .update
}

class MyBaseQuerySet extends QuerySet {
    update(props) {
        validateModelData(props), this.modelClass);
        return super.update(props);
    }
}

MyBaseModel.querySetClass = MyBaseQuerySet;
Owner

tommikaikkonen commented Dec 19, 2016

New release candidate is 0.9.0-rc.1. That includes the fix for #29 ping @Aryk

The README.md and source for the release is in the branch for this PR. master and the docs on gh-pages are still 0.8, if you want to read the new API reference, you need to clone the repo then npm install and make build and open docs/index.html.

0.9 migration guide has been updated per rc1

I gave the fields issue some thought and decided to leave it more flexible for now - defining attr() for each field is not required but recommended. I moved the definition of getters and setters to the prototype for declared value fields, so model instantiation will be faster if you define the fields.

I chose this way because it's fairly easy to add safety provided by accepting only declared fields. Doing it the other way around would've required a more elaborate system (perhaps building in a validation system customizable by the user), and maintained the backwards incompatibility brought by attr.

If you want the safety of only accepting declared fields, you can use something like this snippet to declare your own base model (referring to issue #59, ping @pronebird). You could also delete undeclared fields with small modifcations before passing it to super.create/super.update

(not tested)

import { Model, QuerySet } from 'redux-orm';

function validateModelData(data, model) {
    const fieldNames = Object.keys(data);
    const declaredFields = model.fields;
    fieldNames.forEach(fieldName => {
        if (!declaredFields.hasOwnProperty(fieldName)) {
            throw new Error(`Got value for undeclared field ${fieldName}`);
        }
    });
}

class MyBaseModel extends Model {
    static create(props) {
        validateModelData(props, this);
        return super.create(props);
    }

    update(props) {
        validateModelData(props, this.getClass());
        return super.update(props);
    }

    // setters delegate to .set
    // .set delegates to .update
}

class MyBaseQuerySet extends QuerySet {
    update(props) {
        validateModelData(props), this.modelClass);
        return super.update(props);
    }
}

MyBaseModel.querySetClass = MyBaseQuerySet;
@markerikson

This comment has been minimized.

Show comment
Hide comment
@markerikson

markerikson Dec 19, 2016

Collaborator

Sweet! I still need to find time to sit down and go through all the 0.9 changes in depth, but I think that resolves my concerns regarding declaring fields and flexibility.

Thanks as always for a fantastic library, and for your responsiveness to comments and suggestions!

Collaborator

markerikson commented Dec 19, 2016

Sweet! I still need to find time to sit down and go through all the 0.9 changes in depth, but I think that resolves my concerns regarding declaring fields and flexibility.

Thanks as always for a fantastic library, and for your responsiveness to comments and suggestions!

@tommikaikkonen tommikaikkonen merged commit c732b5b into develop Jan 5, 2017

@Aryk

This comment has been minimized.

Show comment
Hide comment
@Aryk

Aryk Jan 6, 2017

Just curious, is there a reason to do this check rather then first trying to get the row:

const row = this.rows[index]

and then

if (row) {
etc
}

Is this more performant or just preference? 😄

Aryk commented on src/QuerySet.js in a1c0b38 Jan 6, 2017

Just curious, is there a reason to do this check rather then first trying to get the row:

const row = this.rows[index]

and then

if (row) {
etc
}

Is this more performant or just preference? 😄

@ThaJay

This comment has been minimized.

Show comment
Hide comment
@ThaJay

ThaJay Mar 2, 2017

The last update seems a while ago, Would it be a good idea to remove the rc flag by now?
Has anything changed in the meantime?

ThaJay commented Mar 2, 2017

The last update seems a while ago, Would it be a good idea to remove the rc flag by now?
Has anything changed in the meantime?

@tommikaikkonen

This comment has been minimized.

Show comment
Hide comment
@tommikaikkonen

tommikaikkonen Mar 2, 2017

Owner

I've had nearly zero free time since the last release - March will be a lot better, will sift through PRs and issues any make a new release! Sorry for the delay. Looking to give other people commit rights so these kinds of delays don't happen :)

Owner

tommikaikkonen commented Mar 2, 2017

I've had nearly zero free time since the last release - March will be a lot better, will sift through PRs and issues any make a new release! Sorry for the delay. Looking to give other people commit rights so these kinds of delays don't happen :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment