Skip to content
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

chore: add typedefs #802

Merged
merged 19 commits into from
Dec 10, 2020
Merged

chore: add typedefs #802

merged 19 commits into from
Dec 10, 2020

Conversation

vasco-santos
Copy link
Member

@vasco-santos vasco-santos commented Nov 16, 2020

This PR adds typedefs in most of the core codebase. Libp2p interfaces was updated to leverage its new types.

Github actions replaced travis, which also enables us to just use it for windows 🎉

This brings a set of advantages:

  • enable us to generate typescript declaration files automatically on release with aegir.
  • Intellisense for libp2p users
    • Users will be able to see on the fly the libp2p API, expectations for each functions and associated docs/examples
    • Example
  • Intellisense for libp2p core team
    • Core team will be able to see internal types and expectations of each functions without the need to fly around the code. It also enables other editors features like jump to definition, or pop ups with implementation of a highlighted function
  • Automated docs
    • As a follow up, we should be able to automatically generate the API docs

Follow up work: #830

Needs:

Copy link
Contributor

@jacobheun jacobheun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enable us to generate typescript declaration files automatically on release with aegir.

Will we get these automatically now, do we need to update anything for the release?

What are we missing / What is next?

For what's missing, has this been checked against any downstream TS projects? The intellisense is nice, but if we're going to be generating the types on release we should make sure to test this on a larger existing project. For example, can we swap out the types here in Lodestar, https://github.com/ChainSafe/lodestar/tree/master/packages/lodestar, for the types at https://github.com/ChainSafe/libp2p-ts.git that it's using in dev?

@@ -30,7 +29,7 @@ class Book {
/**
* Map known peers to their data.
*
* @type {Map<string, Array<Data>}
* @type {Map<string, Array<*>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of any, *, should these be generics (T)? Based on how the books are structured the data type should be consistent for each book.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make Book generic by adding @template T above class definition. That would also make options.eventTransformer bounded.

src/insecure/plaintext.js Outdated Show resolved Hide resolved
@@ -290,7 +264,7 @@ class IdentifyService {
*
* @private
* @param {object} options
* @param {*} options.stream
* @param {DuplexIterable} options.stream
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably track adding a type for Stream in the interfaces module, that should probably live with Connection. While any DuplexIterable should be fine, we really want these to be Stream's

@vasco-santos
Copy link
Member Author

Will we get these automatically now, do we need to update anything for the release?

Pending ipfs/aegir#671 for support on release. But yes, I can do that test by that time

Copy link
Contributor

@Gozala Gozala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added few nits & general feedback

src/address-manager/index.js Outdated Show resolved Hide resolved
*/

/**
* @typedef {Object} AutoRelayProperties
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: In js-ipfs we settled on calling injected deps with Config suffix e.g. AutoRelayConfig`. Might be worth following same convention across both code bases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably mentioning in the one below, right?
The current approach is: xProperties to libp2p references needed and xOptions for user options, so that we can use xOptions on the top level config docs. We can definitely change xOptions to xConfig and I would be happy in having a better name for xProperties if you have any suggestions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bo I was referring to xProperties actually, maybe I’ve misread the code, but it looked like dependencies injected into AutoRelay and we called such things with xConfig e.g.:

https://github.com/ipfs/js-ipfs/blob/530767d85d85b5932abdb87721e395c0a214b148/packages/ipfs-core/src/components/dag/get.js#L8-13

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to get a good way for here. So, in the libp2p stand point we have lots of places where we inject libp2p, or some libp2p components on other libp2p components. Moreover, these libp2p components have configurations that users can modify.
We need to differentiate between them. My initial approach was to make ComponentNameProperties for the libp2p components we provide, and ComponentNameOptions for user options. Naming config for dependencies that are not "configurable" does not seem the best naming to me. However, I am not totally happy with xProperties, so if we can find a better convention would be great

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe AutoRelayComponents then ?

The reason I end up calling those config in js-ipfs is because it is configuration of the component. It is true that user can't modify those but who ever creates a component can provide alternative configuration e.g. in memory repo vs on disk repo etc...

Things that user can configure are referred as PrefixOptions.

I don't have strong feeling about naming convention itself, just would love to settle on the same convention across ipfs and libp2p

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Components seems a good alternative. Will wait for other people to weight in before renaming everywhere

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we should block this due to naming convention. Lets just have a separate discussion thread somewhere instead and we can always come back and rename things.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I agree. But let's keep the discussion open until hugo/jacob comments

src/content-routing.js Outdated Show resolved Hide resolved
@@ -30,7 +29,7 @@ class Book {
/**
* Map known peers to their data.
*
* @type {Map<string, Array<Data>}
* @type {Map<string, Array<*>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make Book generic by adding @template T above class definition. That would also make options.eventTransformer bounded.

@vasco-santos
Copy link
Member Author

vasco-santos commented Nov 25, 2020

The first batch of review comments were addressed. I will test this with lodestar over the week.

Meanwhile, @Gozala created https://www.npmjs.com/package/typed-events.ts for the EventEmitter stuff ❤️ .
After syncing with him, we might want to get the declaration file from there in and use https://www.npmjs.com/package/proper-event-emitter per #761 (we also have emittery and some modules and we need to double check ig we should just use it). Accordingly, I think we can land the typedefs for EventEmitter in a new PR to not block this.

@vasco-santos vasco-santos force-pushed the chore/add-typedefs branch 5 times, most recently from 2b76df2 to 265bf48 Compare December 3, 2020 11:26
@vasco-santos
Copy link
Member Author

vasco-santos commented Dec 3, 2020

@hugomrdias @Gozala @jacobheun

This is ready and already tested successfully with lodestar build.

Unfortunately, it seems that we will not be able to use templates in the PeerStore Book and I needed to add any to the data types for now (more info on: #821 -- I will create an issue in typescript as the aegir type check works ok with #821 templates, but then the *.ts.d are not correctly generated which breaks user builds)

Copy link
Contributor

@Gozala Gozala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @vasco-santos this must have been a lot of work & I can’t wait for this to lang and propagate across code base 🤩

I added bunch of minor comments and called out few instances where use of generics seems more appropriate. I think it’s mostly there, I just would really like type-check action to get uncommented to ensure all of this type-checks.

.github/workflows/main.yml Outdated Show resolved Hide resolved
src/address-manager/index.js Show resolved Hide resolved
*/

/**
* @typedef {Object} AutoRelayProperties
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we should block this due to naming convention. Lets just have a separate discussion thread somewhere instead and we can always come back and rename things.

src/circuit/circuit/hop.js Outdated Show resolved Hide resolved
src/circuit/circuit/stop.js Outdated Show resolved Hide resolved
@@ -68,7 +71,7 @@ class Stats extends EventEmitter {
/**
* Returns a clone of the current stats.
*
* @returns {Map<string, Stat>}
* @returns {Object}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @returns {Object}
* @returns {Record<string, Stat>}

Assuming Stat is defined somewhere.

Copy link
Member Author

@vasco-santos vasco-santos Dec 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stat is not defined yet. Metrics still have a lot of work to be done as follow up and as this is internal code I did not prfioritize getting through all this now.

src/peer-store/address-book.js Outdated Show resolved Hide resolved
class Book {
/**
* The Book is the skeleton for the PeerStore books.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like Book here supposed to be generic with Data type parameter e.g.

/**
  * @template Data
  */
class Book {
  set(peerId:PeerId, data:Data[]|Data):void
  get(peerId:PeerID):Data[]|undefined
   ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should probably have a composed template. I started doing it and it was working good here. However, when I tried to integrate this in lodestar, I was getting issues on building their project. You have more information on this in #802 (comment) . I have it done in #821 and I already talked with @hugomrdias about it. It seems like a typescript issue (I need to open an issue there), and we should track it before changing it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have looked into this & turns out this was different issue fixed in typescript@4.1.x. So, I have submitted #831 against #821, which addresses this problem and fixes few type miss matches that have came up.

I think it would be good to take a look at that pull as it also uncovered some issues that I have missed when reviewing this.

src/ping/index.js Outdated Show resolved Hide resolved
* @param {Record} other
* @returns {boolean}
* @param {PeerRecord} other
* @returns {other is Record}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since other is already required to be a PeerRecord instance which implements Record interface it is granted so this return type will have no effect.

Suggested change
* @returns {other is Record}
* @returns {boolean}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It errors with:

Property 'equals' in type 'PeerRecord' is not assignable to the same property in base type 'Record'.
  Type '(other: PeerRecord) => boolean' is not assignable to type '(other: any) => other is Record'.
    Signature '(other: PeerRecord): boolean' must be a type predicate.

Per https://github.com/libp2p/js-libp2p-interfaces/blob/feat/add-types/src/record/types.ts#L20

Should I just change the param to any?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so there are couple of things here:

  1. That error occurs because interface says you can pass anything to the method, but then implementation contradicts it by saying by saying you only pass PeerRecord, implying that you can't pass number while interface says you can do that. In other words you can accept wider range of inputs (superset types) in implementations than type / interface requires and be compatible, but you can not do other way around.

  2. I'm honestly confused by equals(other: any): other is Record. Using other is Type kind of annotations are only useful for predicates that are meant to be used for type refinements e.g. example illustrating this

     interface Duck {
       quack(): void
     }
    
     interface Dog {
       bark(): void
     }
    
     const isDuck = (value:any):value is Duck =>
       value && typeof value.quack === 'function'
    
     export const workingExample = (value:Dog|Duck) => {
       if (isDuck(value)) {
         // In this block type of value is `Duck`
         value.quack()
       }
    
       if (value && typeof value.quack === "function") {
         //                      ^^^^^ Property 'quack' does not exist on type 'Duck | Dog'. 
         value.quack()
         //    ^^^^^ Property 'quack' does not exist on type 'Duck | Dog'.
       }
     }

    Note that in some cases on might restrict type of value in predicate in some way (does not need to be any), but the point is that TS essentially treats such predicate functions to do type refinements.

    I am not sure why one would want to do type refinement with equals check e.g. because if type of value in value.equals(other) is known and it's equal to other than value could be used in it's place e.g.:

    const example = (duck:Duck, other:Duck|Dog) {
      if (duck.equals(other)) {
        duck.quack() // as opposed to other.quack()
      }
    }
  3. I think you may want to use Record for parameter type, as in it does not matter if PeerRecord implementation is used or some compatible OtherRecord implementation.

  4. I think generally it is best to avoid any unless you want to tell TS to just shut up about that value. That is because in that case TS will not complain no matter you do with it. I think in most cases actual intent is to say that value is something of any type (via generics interface Record { equals <T>(other:T):boolean } or more simply that value is of unknown type (via interface Record { equals(other:unknown):boolean }). This inspired me to write (what I hope is fun) explainer

If I had to guess you either want equals(other:unknown): boolean or bit more constrained equals(other:Record):boolean in both interface and a class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the example and detailed go through. I am changing the interface + here to unknown and leverage the instance of

Co-authored-by: Irakli Gozalishvili <contact@gozala.io>
@Gozala
Copy link
Contributor

Gozala commented Dec 9, 2020

Unfortunately, it seems that we will not be able to use templates in the PeerStore Book and I needed to add any to the data types for now (more info on: #821 -- I will create an issue in typescript as the aegir type check works ok with #821 templates, but then the *.ts.d are not correctly generated which breaks user builds)

I think the issue you may be referring to is microsoft/TypeScript#41258 which got fixed just yesterday. But in the meantime there is also a workaround that projects can use:
https://github.com/multiformats/js-multiformats#typescript-support

@vasco-santos
Copy link
Member Author

I think the issue you may be referring to is microsoft/TypeScript#41258 which got fixed just yesterday.

Thanks! I think it is that! ❤️ Sadly, they did not release it yet.

But in the meantime there is also a workaround that projects can use:
https://github.com/multiformats/js-multiformats#typescript-support

I feel it is better to add that as a follow up PR. Seems odd to me to release 0.30 with type definitions for TypeScript as one of the headlines, but then people need to skipLibCheck it. I would rather get it in when the new typescript is released as a patch.

What do you think? Any concerns on this approach? From the user stand point, not having template specified in the book yet will not have influence as they will use the book implementations that export the correct types.

@Gozala Gozala mentioned this pull request Dec 9, 2020
7 tasks
Copy link
Contributor

@Gozala Gozala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? Any concerns on this approach? From the user stand point, not having template specified in the book yet will not have influence as they will use the book implementations that export the correct types.

src/circuit/circuit/hop.js Outdated Show resolved Hide resolved
src/circuit/circuit/hop.js Outdated Show resolved Hide resolved
src/circuit/circuit/hop.js Outdated Show resolved Hide resolved
src/circuit/circuit/hop.js Outdated Show resolved Hide resolved
src/circuit/circuit/stop.js Outdated Show resolved Hide resolved
src/index.js Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
@@ -583,7 +645,9 @@ class Libp2p extends EventEmitter {

// Transport modules with discovery
for (const Transport of this.transportManager.getTransports()) {
// @ts-ignore Transport interface does not include discovery
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitted a followup issue #829

src/index.js Outdated Show resolved Hide resolved
@@ -1,3 +1,4 @@
// @ts-nocheck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #830 for that.

src/circuit/transport.js Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants