Skip to content
This repository was archived by the owner on Jan 25, 2022. It is now read-only.

Allow to mark class fields as readonly #83

Closed
bschaepper opened this issue Feb 24, 2018 · 48 comments
Closed

Allow to mark class fields as readonly #83

bschaepper opened this issue Feb 24, 2018 · 48 comments

Comments

@bschaepper
Copy link

Similar to Java final, there should be a convenient way to define a field explicitly as non-writable. With not to much unfamiliar syntax, this could look similar to this:

class A {
    const #xValue;
    static const SOME_CONSTANT = 123;

    constructor(x) {
        this.#xValue = x;
    }

    doSomething() {
        this.#xValue = 5; // illegal
    }

}

Readonly fields must either be initialized directly, or in the constructor. Re-assigning them throws an Error, just as reassigning const variables does.

@mbrowne
Copy link

mbrowne commented Mar 2, 2018

I was thinking that decorators could be used for this, but the latest decorators proposal doesn't allow for this (details). It might be possible to implement in a future add-on to the decorators proposal, but I am now thinking it would be better to just add a readonly modifier to this proposal. Functionally the same as what you're suggesting, just a different syntax since I think the word const is overloaded enough already (however my opinion is not set in stone):

class A {
    readonly #xValue;
    static readonly SOME_CONSTANT = 123;
    ...
}

I think readonly properties are an important use case, and it's also important in many cases to be able to set them in the constructor but not allow them to be writable after that. Yes, we've gotten by without it all this time, but that doesn't mean the solutions we've been using are ideal.

Alternatively, the decorators proposal could go ahead and implement a @readonly decorator natively for now, since it should arguably be part of a standard decorators library anyhow.

@bschaepper
Copy link
Author

A matter of taste, and I don't have a strong opinion about the keyword. A decorator would have been fine too. However, I do believe certain things should be first class concepts of a language, and since there are non-reassignable variables, readonly fields are a natural conclusion. Immutability is an important concept in JavaScript, and deserves first class support in my opinion.

@littledan
Copy link
Member

I think this could be implemented as a decorator, and doesn't need to be built-in. It's pretty difficult to come up with built-in semantics that really work the way you'd expect them to, unfortunately.

It's not possible to use a finisher to turn a field non-writable (since finishers run once per class) but decorators could be used to make a constant field by turning it into a getter/setter pair which permits exactly one set (either from the initializer or, if there is no initializer, from the first thing that writes to it).

@ljharb
Copy link
Member

ljharb commented Mar 3, 2018

Can a field decorator add a finisher?

@littledan
Copy link
Member

littledan commented Mar 3, 2018

@ljharb Yes. But this finisher runs once for the whole class.

@mbrowne
Copy link

mbrowne commented Mar 3, 2018

Yeah, in order to use a decorator finisher to implement read-only fields, the decorators proposal would need to include instance-level finishers, which @littledan said (in a different thread) probably won't be included in the first official decorators spec (but could be added in v2). A getter/setter pair that permits only one set might be OK in the meantime (and perhaps even a good thing in the case of something like an id property that you want to write after the first insert to a database, for example -- depending on your philosophy on mutability), but of course the semantic differences are significant. I was aiming for requiring read-only fields to be initialized at the time of instantiation (i.e. either in the property initializer where the field is declared, or at the end of the constructor or immediately after via a post-constructor finisher)...I think this is how it works in most other languages that have read-only properties. Aside from semantics, another difference to keep in mind is that with a getter/setter pair, you'd also need a private field behind the scenes, and it just seems awkward to implement this way when the language already has support for marking a property as writable: false.

I can actually see both kinds of decorators being useful in different situations. If going with @littledan's suggestion, I would probably call the decorator @writeOnce rather than @readonly to avoid confusion.

@bschaepper
Copy link
Author

@littledan thanks for your input, this kind of would work. Relatively easy to implement even, there is a similar great example in the decorator proposal for observable fields.

After thinking a bit about it, I'd say, this stands and falls with anonymity of private fields. At the moment, decorators can create anonymous, thus "hidden", private slots. If this changes, or can be bypassed, immutability is gone too.

However, nothing except time is lost if we do not add a keyword now. The time it takes for a new proposal to add it, and some time for missed JIT optimizations. Baked in immutability would be a huge plus, IMHO.

@doodadjs
Copy link

doodadjs commented Mar 3, 2018

I'd prefer a decorator and to respect the field descriptors semantic, ie @writable(false). That also implies having @enumerable and @configurable.

@mbrowne
Copy link

mbrowne commented Mar 3, 2018

I think it would be best to limit @writable(false) only to cases where you are setting a value right in the field initializer (and not later in the constructor, or waiting until the first write to the field). Otherwise it's too ambiguous...much clearer if @writable(false) means non-writable applied immediately when the field is created. @readonly and @writeOnce would both have a somewhat different meaning than that.

@doodadjs
Copy link

doodadjs commented Mar 3, 2018

@mbrowne But why have a @writable(false) which applies immediately at initialization, and have @readonly which applies after construction at the finisher ?

@mbrowne
Copy link

mbrowne commented Mar 4, 2018

Because it's a useful pattern and that's how read-only properties are implemented in many other languages, so it's more likely to be the programmer's expectation than any of the other alternatives. But I don't know if naming it this way is the best; perhaps it should be more explicit like @immutableAfterConstruction. My main point is that there are different possible meanings and @writable(false) is potentially ambiguous...but of course @readonly is ambiguous too, especially for someone who has never worked with read-only properties before (in any language).

@hax
Copy link
Member

hax commented Mar 5, 2018

For keyword, readonly seems a little bit more "correct" than const. Because you can not write code like:

const x;
// ...
x = 1; // initialize later

Note, when class fields land, it will be very hard to add readonly (or other keyword not in reserved keywords list) later. Because it may cause new ASI hazard.

readonly x;
readonly // <- a field named "readonly"
y;

About the name for decorator, @nonwritablemay be more "js taste"? But I prefer just use @readonly for ergonomics. If a programmer never heard about readonly it's very likely he/she also never used defineProperty({writable: true}) API. 😝

@littledan
Copy link
Member

I still don't really know what semantics you're proposing here. Could you be more specific, in terms of what this would mean in terms of the object model, when you can write to the field, etc? I'm still not sure exactly what benefit we're expecting to get by making this built-in as opposed to based on a decorator, possibly based on the addition of more decorator capabilities.

Would it be OK to pursue this feature in a follow-on proposal? The class fields proposal is already at Stage 3, which means that we hope to be done with the "language design" part and are getting tweaks from actual implementations.

@mbrowne
Copy link

mbrowne commented Mar 5, 2018

I think it depends on the overall strategy. If in the future there will be other modifier keywords for class fields and methods (e.g. protected just as one example - in a previous thread we said that the only really problematic modifier was private and others could potentially be added in the future) then I think it makes sense to add readonly now as @hax suggested -- or at least make it a reserved word in the context of class field declarations, to allow for adding it later. It would also make sense to reserve words for possible future access modifiers like protected, friend, and internal

...but now that I think about it, this introduces a new issue to discuss, which is that someone could legitimately want a property named any of these things, or might already have properties with these names in code they wrote using the current class-properties Babel plugin or TypeScript. I just tried it in TypeScript, and actually they don't have any restrictions on this: e.g. you can have a private string property named private by writing private private: string. Maybe the ASI hazard @hax mentioned is an acceptable hazard of the syntax; it seems to be OK for TypeScript users.

Anyway, I also wanted to offer another alternative to consider, which is that the decorator syntax could be used for all of these things, even for features that might be native in the future. There could be a special namespace or syntax to indicate that, e.g. @native.readonly or @@readonly. This is especially relevant for features that could only be awkardly and imperfectly implemented with userland decorators, e.g. protected. Maybe it could be as simple as importing the decorator from a special module, e.g.:

import { protected, friend } from 'native-decorators'

@hax
Copy link
Member

hax commented Mar 5, 2018

Yes TS guys decide to limit the keywords public private protected should not follow newline like async. They even put the same limitation to get which not compatible with JS (don't know whether it's a bug or by design).

@bschaepper
Copy link
Author

bschaepper commented Mar 5, 2018

@littledan semantics I had in mind is more or less analogous to Java and others: Readonly fields must either have an initializer, or be initialized in the constructor (but not both). After once assigned a value, it cannot be re-assigned. Inherited private properties are not relevant, since completely invisible to a subclass. Public fields can not be shadowed or overridden by child classes, re-defining a readonly property should throw an error. (Same actually goes for method definitions, readonly would be useful there too. Where would be the right place to discuss this?)

Today I would have to use Object.createProperty in the constructor to do this, but that's too cumbersome to me and therefore I don't do it. For static class constants it is even less obvious, as this has to be done after the class is defined. To communicate intent, allow for some JIT optimizations and prevent mistakes I would like to use it in some simple fashion.

Why this is helpful: Immutable value objects, immutable services etc. Often a field is initialized in the constructor, and not intended to ever be overwritten. Most fields in our code are not intended to be re-assigned after IOC has wired up everything.

@hax you are right, was not thinking about ASI. At the very least reserving potential modifiers would not hurt. I think we might could learn a lesson from PHP here, where they got class fields entirely wrong at first.

As I said earlier, I think some concepts should be first class. Immutable variables are, so immutable fields should be too. Decorators can help with many things, but this I'd rather have one single first class facility.

@hax
Copy link
Member

hax commented Mar 6, 2018

Readonly fields must either have an initializer, or be initialized in the constructor (but not both)

Should we allow this?

class A {
  readonly x
  constructor() {
    this.x = 1
    ...
    this.x = 2
  }
}

Sometimes I think allow mutate readonly fields in the constructor is convenient.

And should we allow this?

class A {
  readonly x
  constructor() {
    this.init() // call other methods
  }
  init() { 
    this.x = 1 // init() should only be called by constructor, or it will throw runtime error here
  }
}

It's common to do initializations in other methods, especially when there are many states need to initialize, for example, deal with complex options object.

And should we allow this?

class A {
  readonly x
}
const a = new A // `x` not initialized, throw TypeError here?
a.x // `undefined` or throw TypeError?
a.x = 1 // can we ?

Note, it seems not easy possible to support any of them by decorator.

@bschaepper
Copy link
Author

@hax first example, where would this be useful? I've never encountered it, and I'm suspicious.. I'd probably even rate it as a code smell in a review. Thus, no, I would specifically think this should not be allowed.

Your second example looks useful sometimes, but also seems very confusing. Id rather not allow it.

In the third example, the field should just be undefined, and not throw an error. Trying to set it "from the outside" should always throw, even if not assigned (is undefined).

@hax
Copy link
Member

hax commented Mar 6, 2018

@bschaepper
I myself also don't very like the codes in my examples. But I used to see them. Of coz example1 and example2 can be refactored to some like:

class A {
  readonly x
  constructor() {
    this.x = this.initX()
  }
  initX() { 
    let x = 1
    ...
    if (...) x = 2
    return x
  }
}

But that means many old codes need to refactor first before add readonly.

@mbrowne
Copy link

mbrowne commented Mar 7, 2018

@hax I think your second example should be allowed, but not the others. It often improves code readability to have helper methods called by the constructor that initialize some of the fields. And I think it would be easy to implement with decorators if a future version of decorators allowed instance-level finisher functions. In that case you could just set all the @readonly fields to writable: false immediately after the constructor finishes. That does leave the problem of your first example though...I think the only way to prevent that without native support would be a linting rule. But personally I would be OK with that.

I think the bigger question to be answered here is whether or not some possible future modifier keywords (including readonly) should be reserved words in the context of field/method declarations. I don't have a strong opinion on this point because I'm not sure what strategy is best...any thoughts on what I said above about native decorators?

@littledan
Copy link
Member

This semantic discussion is great, but we have to think about how it will be represented in the JavaScript object model as well as within the lifecycle here of superclass and subclass constructors. We can't apply the Java model here, as I believe that is based on a static analysis of how many times the instance variable is written to.

@mbrowne
Copy link

mbrowne commented Mar 7, 2018

Perhaps an example will help to discuss what the specification should be. Let's suppose this is the code we'd like to write:

class Person {
    readonly id
    firstName
    lastName
    email

    constructor({ id, firstName, lastName, email }) {
        this.id = id
        this.firstName = firstName
        this.lastName = lastName
        this.email = email
    }
}

class User extends Person {
    readonly dateRegistered
    passwordEncoded

    constructor(attributes) {
        super(attributes)
        this.dateRegistered = attributes.dateRegistered
        this.passwordEncoded = attriutes.passwordEncoded
    }
}

Here is how I am thinking it could work under the hood - demonstrated in ES6:

const defaultDescriptor = {
    enumerable: true,
    configurable: false,
    writable: true,
    value: undefined
}

class Person {
    constructor({ id, firstName, lastName, email }) {
        Object.defineProperties(this, {
            id: {
                enumerable: true,
                configurable: true,
                writable: true,
                value: undefined
            },
            firstName: defaultDescriptor,
            lastName: defaultDescriptor,
            email: defaultDescriptor,
        })

        this.id = id
        this.firstName = firstName
        this.lastName = lastName
        this.email = email
        // don't run finisher yet for subclasses
        // (we only want to run it after all other constructor code has finished)
        if (new.target === Person) {
            this.__finisher()
        }
    }

    __finisher() {
        Object.defineProperty(this, 'id', {
            enumerable: true,
            configurable: false,
            writable: false,
            value: this.id
        })
    }
}

class User extends Person {
    constructor(attributes) {
        super(attributes)
        Object.defineProperties(this, {
            dateRegistered: {
                enumerable: true,
                configurable: true,
                writable: true,
                value: undefined
            },
            passwordEncoded: defaultDescriptor,
        })

        this.dateRegistered = attributes.dateRegistered
        this.passwordEncoded = attributes.passwordEncoded

        if (new.target === User) {
            this.__finisher()
        }
    }

    __finisher() {
        super.__finisher()
        Object.defineProperty(this, 'dateRegistered', {
            enumerable: true,
            configurable: false,
            writable: false,
            value: this.dateRegistered
        })
    }
}

(Side-note: In a real app I would probably have some sort of convenience mechanism to set the initial state of the object [the above example is a little verbose/repetitive], but not just Object.assign() because that would allow the consumer to set properties that don't belong on the object. It would be nice to do this automatically in a base class. Since there will be no out-of-the-box reflection for class properties at least in the initial version, superclasses could be made aware of defined properties on subclasses via a decorator on every externally settable property, e.g. @attribute...but that's a separate discussion.)

@mbrowne
Copy link

mbrowne commented Mar 7, 2018

P.S. I realize that my proposed design as shown in the ES6 example doesn't prevent @hax's first example, which I said should be illegal:

class A {
  readonly x
  constructor() {
    this.x = 1
    ...
    this.x = 2
  }
}

As I said above, that could be prevented with a linting rule. However, if readonly properties are natively supported rather than implemented with a userland decorator, it would be ideal if the JS engine made the property read-only immediately after the first write in the constructor (i.e. you can't set it multiple times in the constructor, only once).

@bschaepper
Copy link
Author

bschaepper commented Mar 8, 2018

@mbrowne Very nice illustration, similar to what I was thinking. However, I'd set fields to writable: false and configurable: false immediately, removing the need for finishers altogether.

Part of the goal for readonly properties is, to not allow child classes to override anything marked as readonly.

An assignment to readonly properties thus always comes down to this:

Object.defineProperty(this, <fieldname>, {
    enumerable: true,
    configurable: false,
    writable: false,
    value: <value>
})

This greatly simplifies semantics, IMHO, making readonly a very simple thing. @littledan does this illustrate what is supposed to happen for you, or shall I try to spec this out, in a PR maybe?

@mbrowne
Copy link

mbrowne commented Mar 8, 2018 via email

@bschaepper
Copy link
Author

You are right, @mbrowne. I'm very sorry but I have to ask: From the "Evaluation Order Proposal" references in decorators-proposal my understanding is, 1. constructor is executed 2. field decorators are executed, 3. initializers are executed. (leaving out irrelevant details). Is this correct? If so, I am not certain this is the best way. First of all, I can not reference field values with an initialzer, they are undefined while the constructor is evaluated, right? Furthermore, they are not yet decorated, or are they?

If I'm not mistaken, this is a problem, or very unexpected at least. Probable bugs whenever I set a value in the constructor, since decorations are not ready, or use a field in the constructor, since values are not initialized yet.

Isn't that extremely confusing? Up until re-reading the evaluation order I was under the impression, initializers and decorators should run before one gets a chance to use them in the constructor. Would make a lot more sense from a user perspective IMHO.

@mbrowne
Copy link

mbrowne commented Mar 10, 2018

@bschaepper It looks like that "Class Evaluation Order" PowerPoint presentation (assuming that's what you're referring to) was written a while ago, without class properties in mind. I think the better place to look to understand the evaluation order would be this proposal (which says it includes everything that was previously in the class evaluation order proposal). My understanding is that declared fields are created before the constructor runs, at least if we're talking about just one class. If there is a superclass, then its constructor won't know anything about instance-level fields in subclasses because they don't exist yet (I was actually a bit concerned about that as I explained in #36, but there are other ways to enable reflection on subclass properties, such as decorators or alternatively flow types made available at runtime).

Also note that initializers aren't just for decorators; it's just reusing the initializers that are already part of this proposal. If you have:

class MyClass { x = 1 }

then the initializer is setting x to 1. Decorators are passed an object that includes the initializer, which they can override if they want.

@mbrowne
Copy link

mbrowne commented Mar 16, 2018

About ASI (automatic semicolon insertion)...Suppose that a future version of JS has readonly and protected modifiers for fields (among others, possibly). Would there be any real benefit to allowing this syntax:

class Demo {
  readonly
    foo;
  protected
    bar = 1;
}

I think it might be better to just enforce that the modifier and the field name are always together on the same line, similarly to the requirement for async (you can't have a newline between async and your function). That way there's no need for reserved keywords, and both of the following would be valid code:

class Demo {
  // a field named 'readonly'
  readonly = true
}

class Demo2 {
  // a readonly field named 'foo'
  readonly foo
}

It would also be one less thing to add to this proposal, helping it get out the door (not that reserving keywords is a big effort, but still...). Read-only properties could be a later add-on to this proposal. Or alternatively, included in a standard decorators library with a future version of decorators.

@bschaepper
Copy link
Author

@mbrowne totaly right about ASI. I'd say it's a great idea to disallow newlines in field declarations anyway. IIRC comma separated fields have been dropped for similar reasons, no?

Thanks for your clarification on the evaluation order too, it does make sense as you state it. If my understanding about decorators is correct, it can always replace the field definition with its own - which makes setting properties to non-writable non-configurable no problem, right? Or did I get something about the specs wrong :-)

@mbrowne
Copy link

mbrowne commented Mar 21, 2018

If my understanding about decorators is correct, it can always replace the field definition with its own - which makes setting properties to non-writable non-configurable no problem, right? Or did I get something about the specs wrong :-)

It's an issue of timing. If the decorator sets the property to non-writable and non-configurable when it's being created, then it won't be possible to set a different value via a constructor argument (e.g. this.x = someArgument), which happens later. To recap the order (speaking about just one class with no parent classes to keep it simple):

  1. the properties are initialized (this is equivalent to Object.defineProperty())
  2. the constructor runs

If instance-level finishers are added to the decorators proposal, then we would have an additional step 3 where decorators could make further modifications to property descriptors.

But that doesn't address your concerns about child classes re-defining a readonly property that was declared in a parent class. I think native support would be required for that, or at least I can't think of a good way to do it with decorators.


Update: Well, there is one possible way: if decorators would have a special ability to recreate a non-configurable field. But currently they don't.

@doodadjs
Copy link

doodadjs commented Mar 21, 2018

@mbrowne IMO, modifiers and identifiers can be separated by newlines, that must not be enforced.

@mbrowne
Copy link

mbrowne commented Mar 21, 2018

@doodadjs Why? The same-line restriction seems to work fine for async.

@doodadjs
Copy link

@mbrowne didn't know about "async function"... Anyway, that's something that must be enforced by a linter, not the language itself.

@mbrowne
Copy link

mbrowne commented Mar 21, 2018

For async it's enforced by the language itself - no linter required - at least in node.

@mbrowne
Copy link

mbrowne commented Mar 21, 2018

Also in Chrome (just tested it), and presumably any browser that supports async functions.

@doodadjs
Copy link

Tested on Firefox, and browsers too. Doesn't mean it was a good idea, but anyway...

@hax
Copy link
Member

hax commented Mar 21, 2018

@doodadjs

that's something that must be enforced by a linter, not the language itself.

TypeScript already enforced it. (readonly, public, private... and even get/set though JavaScript allow it cross-lines)

@hax
Copy link
Member

hax commented Mar 21, 2018

@mbrowne

Well, there is one possible way: if decorators would have a special ability to recreate a non-configurable field. But currently they don't.

I think it's possible to revise new semantic to postpone the decorator finishers after all constructor callings.

@littledan
Copy link
Member

OK, I still don't see a workable proposal for semantics in this thread. Would it be possible to pursue this as a follow-on proposal?

@mbrowne
Copy link

mbrowne commented Mar 28, 2018

Would it be possible to pursue this as a follow-on proposal?

I think that's a good plan. The only thing that would need to be done now rather than later is reserve certain modifier words including readonly -- but as I said above, I don't think it's necessary to support newlines in between the modifier and the field name. And I'm not sure yet whether a modifier keyword is even the best syntax for this...it seems like access levels like protected and friend will be achieved via decorators, at least initially, and I think readonly or other words affecting the field should have a similar syntax as those.

So currently I'm leaning toward a @readonly decorator. Of course that would require instance-level finishers for decorators...hopefully those can be added sooner rather than later, although I realize they probably won't make it into the first version.

@littledan
Copy link
Member

OK, anything more to do in this bug thread, or should we close it?

@mbrowne
Copy link

mbrowne commented Mar 28, 2018

@bschaepper?

@bschaepper
Copy link
Author

Whether decorator or keyword does not matter too much, I just want some way to have readonly properties. Insofar, @littledan, close the issue if you think this poses no major roadblock for a follow-on proposal

Overall I find it hard to believe, that this is the best we can come up with:

class Foo {
    @readonly static TYPE = "FooType";
    #field = 1;
    @protected @readonly #otherField = Foo.TYPE;
    fooValue;
}

Very noisy, and overall implicit and inconsistent notation. Private, protected and public are all completely different. Very awkward. I honestly can not follow why this seems like a good idea and the right way to go to anybody.

I'd much prefer a cleaner and more consistent syntax. Also making the implicit explicit with modifiers.

class Foo {
    public static readonly TYPE = "FooType";
    private #field = 1;
    protected readonly #otherField = Foo.TYPE;
    public fooValue;
}

@littledan
Copy link
Member

@bschaepper How about this for a gradual evolution path?

  • Add decorators so that it's possible to implement these things with decorators
  • When the decorator becomes really common, consider adding it to the standard library
  • When the usage in the standard library becomes common and is considered ugly, we can consider making it directly within the class syntax in order to eliminate the @ sign

@mbrowne
Copy link

mbrowne commented Mar 29, 2018

To add to what I said above, I'd also be in support of proceeding on two fronts in parallel:

  1. Adding instance-level finishers to decorators
  2. A new proposal for a readonly modifier keyword

Ultimately, I agree it would be best if the most common use cases - the things that are core features in many other OO languages - were supported natively as modifier keywords. @littledan's proposed path makes sense and due to limited time, I personally will probably focus more on the decorator option for now, especially since I think instance-level finishers for decorators would be a good feature to have anyhow. But if anyone wants to draft a proper proposal for native readonly fields, I'll certainly follow that discussion and support the proposal (FWIW).

Also, when I was thinking about modifier keywords I had forgotten about static...that's implemented as a keyword, but access levels other than private (e.g. protected) will initially be implemented as decorators, so it's a bit of a toss-up as to which option for readonly would be more syntactically consistent. As @littledan proposed, it makes sense to give the community a chance to establish common patterns using decorators and subsequently propose to make them native (usually using keywords I imagine, although there could also be native decorators if the decorator syntax is preferred for some particular feature). At some point we need to establish a formal basis for deciding on keywords vs. decorators, but I'll go out on a limb and say that readonly should eventually be a keyword. For one thing, there could be issues getting the semantics exactly right with a userland decorator (even with the addition of instance-level finishers), as I mentioned in previous comments here...but it's a good first step.

@mbrowne
Copy link

mbrowne commented Mar 29, 2018

BTW, I think the private #x syntax was previously proposed and rejected for some reason, is that right?

@bschaepper
Copy link
Author

bschaepper commented Mar 29, 2018

@littledan your approach seems right 😊 I'm looking forward to finally having simple and real private state in JavaScript. I know it must be a bit frustrating at times, but keep up your good work 😊 👍

@mbrowne thanks for the constructive discussion 😉

@littledan
Copy link
Member

Glad we could all get on the same page about next steps. See CONTRIBUTING.md for how to keep going here.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants