Skip to content
This repository has been archived by the owner on Jun 26, 2023. It is now read-only.

feat: add types #74

Merged
merged 23 commits into from
Dec 10, 2020
Merged

feat: add types #74

merged 23 commits into from
Dec 10, 2020

Conversation

vasco-santos
Copy link
Member

@vasco-santos vasco-santos commented Dec 1, 2020

This PR adds typedef improvements for the already available modules, as well as typescript files for the interfaces needed in libp2p core per libp2p/js-libp2p#802

  • Aegir updated to latest version to support types generation
    • resulting linting issues fixed
  • Types for interfaces: Transport, Muxer and Crypto
    • should probably look into uniforming the language on the types and Readme
  • Switched from travis to Github actions

On top of @Gozala 's #73 PR

Needs:

  • type declaration files removed (currently in place to be used by libp2p PR)

BREAKING CHANGE: Some types from connection, pubsub and topology are now fixed. Record is not a class anymore, but a typescript interface that should be implemented

@vasco-santos vasco-santos force-pushed the feat/add-types branch 2 times, most recently from e37aec0 to f2ab5b9 Compare December 1, 2020 22:10
@Gozala Gozala mentioned this pull request Dec 2, 2020
Copy link

@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.

@vasco-santos I did a pass here, but I think I have a more general feedback that is not pinned to any specific line:

  • aegir bulid will generate types in dist so no .d.ts files are necessary (I did pushed a commit here that removes them, but feel free to revert it if you disagree).
  • types in dist need to be mapped in package.json (I have created Feat/add types 2 #75 that does it).
  • type check / build is failing with this tree (I have created Feat/add types 2 #75 that addresses remaining issues)
  • I think we need to distinguish between interfaces (interface { ...}) and classes that implement those. And users of those implementations should code against interfaces not implementation classes. That enables anyone to do a different implementation of the interface, ensure with type checker compatibility and use it as drop-in replacement.
  • I think declare class-es must go, if they are necessary they should be interfaces.
  • There are some incompatibilities regarding MuxedStream and DuplexIterableStreams (I had to suppress some mismatches in Feat/add types 2 #75). I think we should resolve those. I don't have a complete enough understanding in this area to provide specific suggestions, but would love to have session so I can better understand it and also help resolve this.
  • There are bunch of places where instance fields are null|T but in code used as T (I have suppressed those with @ts-ignore in Feat/add types 2 #75, but I think we need a followup pull to resolve them)

package.json Outdated Show resolved Hide resolved
src/crypto/types.ts Outdated Show resolved Hide resolved
src/record/index.js Outdated Show resolved Hide resolved
src/pubsub/index.js Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
tsconfig.json Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/pubsub/index.js Outdated Show resolved Hide resolved
src/pubsub/peer-streams.js Outdated Show resolved Hide resolved
src/pubsub/index.js Outdated Show resolved Hide resolved
src/crypto/types.ts Outdated Show resolved Hide resolved
src/connection/connection.js Outdated Show resolved Hide resolved
@vasco-santos
Copy link
Member Author

Thanks for the reviews. On @Gozala feedback:

aegir bulid will generate types in dist so no .d.ts files are necessary (I did pushed a commit here that removes them, but feel free to revert it if you disagree).
types in dist need to be mapped in package.json (I have created #75 that does it).

Yes, as mentioned on the PR desc, I kept them to test it with libp2p/js-libp2p#802
Unfortunately this will be a limitation for testing stuff across multiple PR dependencies.

I think we need to distinguish between interfaces (interface { ...}) and classes that implement those. And users of those implementations should code against interfaces not implementation classes. That enables anyone to do a different implementation of the interface, ensure with type checker compatibility and use it as drop-in replacement.
I think declare class-es must go, if they are necessary they should be interfaces.

I added them to go around the type checker error of the type not being constructable. Anyway, this did not fix the issue, so I will remove them.

There are some incompatibilities regarding MuxedStream and DuplexIterableStreams (I had to suppress some mismatches in #75). I think we should resolve those. I don't have a complete enough understanding in this area to provide specific suggestions, but would love to have session so I can better understand it and also help resolve this.

Yeah, I think they should all be MuxedStream, if you are talking about Pubsub. I will change them

There are bunch of places where instance fields are null|T but in code used as T (I have suppressed those with @ts-ignore in #75, but I think we need a followup pull to resolve them)

I will have a look on these

Copy link

@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.

Add few nits, but looks good to me to go with or without them.

src/pubsub/message/sign.js Outdated Show resolved Hide resolved
src/pubsub/message/sign.js Outdated Show resolved Hide resolved
* @typedef {import('./message').Message} RPCMessage
* @typedef {import('./signature-policy').SignaturePolicyType} SignaturePolicyType
*/

/**
* @typedef {Object} InMessage
* @property {string} [from]
Copy link

Choose a reason for hiding this comment

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

Following code seems to suggest that from isn't optional here
https://github.com/libp2p/js-libp2p-interfaces/pull/74/files#diff-2398b30d2245a825770bfde7b95fd0b65f3ce6435c3f8cd87a632f583a5b2addR66

If so can we turn it into non optional so type checker can report the error instead of leaking it into runtime ?

Copy link
Member Author

@vasco-santos vasco-santos Dec 3, 2020

Choose a reason for hiding this comment

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

It is really dependent on the signature policy. Have a look into the validate function. It will be undefined or should exist according to that policy being used.
The same for other places like the verifySignature function, which validate only calls if it has a signature. I will add an error there to avoid potential future usages of the function though

src/pubsub/peer-streams.js Outdated Show resolved Hide resolved
@@ -138,26 +140,26 @@ class PeerStreams extends EventEmitter {
/**
* Attach a raw outbound stream and setup a write stream
*
* @param {Stream} stream
* @param {MuxedStream} stream
* @returns {Promise<void>}
*/
async attachOutboundStream (stream) {
// If an outbound stream already exists,
// gently close it
const _prevStream = this.outboundStream
Copy link

Choose a reason for hiding this comment

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

In that case this line is no longer needed

Suggested change
const _prevStream = this.outboundStream

Copy link
Member Author

Choose a reason for hiding this comment

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

We need it for the check in the end of the function to only emit stream:outbound if the connection is new. We should only emit it when the outbound stream is ready, so this should be in the end, which means we need that auxiliary constant

@@ -7,25 +7,31 @@ const errcode = require('err-code')
*/
class Record {
Copy link

Choose a reason for hiding this comment

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

If thing does not need to be a class it's best to define it as interface / type because it could be substituted with other implementation or even just plain object that conforms the interface. Classes should really just be a one possible implementation of it. Current version implies that TS will error anywhere record instanceof Record isn't true.

* Checks if the given value is a `MulticodecTopology` instance.
*
* @param {any} other
* @returns {other is MulticodecTopology}
Copy link

Choose a reason for hiding this comment

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

src/transport/types.ts Outdated Show resolved Hide resolved
@Gozala
Copy link

Gozala commented Dec 2, 2020

So, my suggestion here is that we punt on this until we get the types into the transports codebase. By that time, we can really see the advantages/limitations and decide if we should add here a second interface for the constructor. What do you thing?

Seeing the prototype field and new in the interface definition, I do really think it would be an easier change to do now than deferring it to a future. It's going to be a couple of lines change but avoid having to propagate changes in the future. I'll provide specific comment in the pull.

Either way it's just a suggestion and it's your decision to make.

@Gozala
Copy link

Gozala commented Dec 2, 2020

Ok if I take the current typedefs for transports and try to implement it like this:

type TCPDial = { signal?: AbortSignal }
export class TCPTransport implements Transport<TCPDial> {
//            ^^ Class 'TCPTransport' incorrectly implements interface 'Transport<TCPDial>'.
  constructor(upgrader: Upgrader, ...others: any) {
    return this
  }
  /**
   * Dial a given multiaddr.
   */
  dial(ma: Multiaddr, options?: TCPDial): Promise<Connection> {
    throw 1
  }
  /**
   * Create transport listeners.
   */
  createListener(options: any, handler: (connection:Connection) => void): Listener {
    throw 2
  }
  /**
   * Takes a list of `Multiaddr`s and returns only valid addresses for the transport
   */
  filter(multiaddrs: Multiaddr[]): Multiaddr[] {
    return multiaddrs
  }
}

P.S.: It complains about the prototype field but if you remove prototype from interface it will still complain about new.

Now consider what I was suggesting instead:

type TCPDial = { signal?: AbortSignal }
export class TCPTransport implements Transport<TCPDial> {
  constructor(_upgrader: Upgrader, ..._others: any) {
  }
  /**
   * Dial a given multiaddr.
   */
  dial(_ma: Multiaddr, _options?: TCPDial): Promise<Connection> {
    throw 1
  }
  /**
   * Create transport listeners.
   */
  createListener(_options: any, _handler: (connection:Connection) => void): Listener {
    throw 2
  }
  /**
   * Takes a list of `Multiaddr`s and returns only valid addresses for the transport
   */
  filter(multiaddrs: Multiaddr[]): Multiaddr[] {
    return multiaddrs
  }
}

export const dial = <T extends { signal: AbortSignal }>
  (NetworkTransport: TransportFactory<T>, upgrader: Upgrader, ma:Multiaddr, options:T):Transport<T> => {
  const transport = new NetworkTransport(upgrader)
  transport.dial(ma, options)
  return transport
}

Unlike above example It allows implementation of the transport to claim that it implements the Transport and type checker will make sure of it and all was necessary is to separate concerns by splitting interface into two:

interface TransportFactory <DialOptions extends { signal?: AbortSignal }> {
  new (upgrader: Upgrader, ...others: any): Transport<DialOptions>;
}

/**
 * A libp2p transport is understood as something that offers a dial and listen interface to establish connections.
 */
export interface Transport <DialOptions extends { signal?: AbortSignal }> {
  /**
   * Dial a given multiaddr.
   */
  dial(ma: Multiaddr, options?: DialOptions): Promise<Connection>;
  /**
   * Create transport listeners.
   */
  createListener(options: any, handler: (connection:Connection) => void): Listener;
  /**
   * Takes a list of `Multiaddr`s and returns only valid addresses for the transport
   */
  filter(multiaddrs: Multiaddr[]): Multiaddr[];
}

/**
* Create transport listeners.
*/
createListener(options: any, handler: (Connection) => void): Listener;
Copy link

Choose a reason for hiding this comment

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

looks like options here should be also made into generic rather than any.

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 can change it to unknown, but really not much more.
Taking a look at https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/transport#create-a-listener there is no interface options for the listener. Each transport will have their own.

Copy link

@Gozala Gozala Dec 3, 2020

Choose a reason for hiding this comment

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

But that also the whole point of generics to define a generic transport interface so that specific ones can specify concrete types that correspond to them just like it was in the case of dial:

interface Tansport<DialOptions extends { signal: AbortSignal }, ListenerOption> {
   dial(ma: Multiaddr, options?: DialOptions): Promise<Connection>;
   createListener(options: ListenerOptions, handler: (Connection) => void): Listener;
    filter(multiaddrs: Multiaddr[]): Multiaddr[];
}

class TCPTransport implements Transport<TCPDialOptions, TCPListenerOptions> {
   dial(ma: Multiaddr, options?: DialOptions): Promise<Connection> {
     // ...
   }
   createListener(options: TCPListenerOptions, handler: (connection: Connection) => void): Listener {
      // ...
   }
   // ...
}

type TCPDialOptions = {
  // ...
}

type TCPListenerOptions = //...

You could even go step further and make Transport have a default type e.g.:

interface Transport <DialOptions extends AbortOptions = AbortOptions, ListenerOptions = void> {
}

// Will be a shortuct for implemnets TCPTransport<AbortOptions, void>
class TCPTransport implements Transport {
} 

src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
src/transport/types.ts Outdated Show resolved Hide resolved
@hugomrdias
Copy link
Member

Ts check is just commented out in the config because it's fail if a repo doesn't have the tsconfig (looking at you @Gozala this could be better 🙏🏼)
Regarding the transport issue I don't really understand the dial suggestion but seems that at least the prototype and new should go into a separate type *Factory or *Construtor something like that

@vasco-santos
Copy link
Member Author

So, I did a new iteration on this with the latest feedback from @Gozala 👍 :

  • Record is now just an interface
  • Transport and StreamMuxer now have a Factory interface for the constructor
  • Pubsub minor fixes

This should now be ready to go.

@vasco-santos vasco-santos requested review from hugomrdias and removed request for hugomrdias December 3, 2020 10:27
.github/workflows/main.yml Outdated Show resolved Hide resolved
Co-authored-by: Hugo Dias <hugomrdias@gmail.com>
@Gozala
Copy link

Gozala commented Dec 3, 2020

Ts check is just commented out in the config because it's fail if a repo doesn't have the tsconfig (looking at you @Gozala this could be better 🙏🏼)

There is tsconfig.json so can't we enable it ? I'm sorry but I really don't want to deal with repos that do not have tsconfig.json because it needs to report same errors as running tsc locally. If we don't want to have a tsconfig.json in repo we could just run aegir ts --preset config > tsconfig.json prior to running the checker.

@Gozala
Copy link

Gozala commented Dec 3, 2020

Regarding the transport issue I don't really understand the dial suggestion but seems that at least the prototype and new should go into a separate type *Factory or *Construtor something like that

Have you seen this comment #74 (comment) it contains links to the TS playground illustrating issues. IMO whole point of the interface is to be able to tell in the class implementation that it implementns some interface. Current version is not implementable (or more like it's not doing what I think assumbtion is about it). There is also a second link that illustrates how it could be split into two interfaces such that class can say it implements Transport and the other TrasportFactory (or whatever you want to call it) that can be used in places that need a thing that instantiates a Transport.

P.S.: prototype field isn't required by either interface.

@vasco-santos
Copy link
Member Author

Per libp2p/js-libp2p#802 feedback, I added two new minor commits to fix review issues

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

Successfully merging this pull request may close these issues.

3 participants