Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Discussion: Improving typings #3413

@Gozala

Description

@Gozala

Constraints

Here is the set of constraints following proposal attemts to satisfy (please call
out if you find something is missing or doesn't belong)

  • Provide a way to ensure that ipfs-core and ipfs-http-client implement
    same base API.
    • They could intentionally diverge a bit (e.g. http-client takes extra HTTPOptions)
      and so could ipfs-core but there should be common subset between two.
      • Compatibility should be ensured type checker as opposed to discipline or
        peer review
  • Should require no type annotation repetitions.
  • Types should be simple and stright forward, meaning avoid
    magic types
    that are hard to understand.
  • Cross package type dependecies should not depend on internal file structure
    (as much as possible)

Proposal

I propose to satisify all of the listed constraints by doing following

  1. Create a designated directory for reusable type definitions (e.g.
    src/interface or whatever wins bikeshed contest and have component specific
    interface definitions for ipfs.pin, ipfs.files, ipfs.dag, etc..

    Note: This still creates file structure but it is agnostic of implementation
    details. Alternative could be to use TS namespaces,
    but they are left over from pre-esm days in TS and seems like non futureproof
    approach, which is why using naming convetion seems like a better option
    ipfs.pin -> interface/pin, ipfs.files -> interface/files, etc...

    Below is illustration of what interface/pin.ts could look like:

    import CID from "cids"
    import { AbortOptions } from "./utils"
    
    export interface API {
      addAll (source: ToPins, options?: AddOptions & AbortOptions): AsyncIterable<CID>
      add(source: ToPin, options?: AddOptions & AbortOptions): Promise<CID>
      rmAll(source: ToPins, options?: RemoveOptions & AbortOptions): AsyncIterable<CID>
      rm(source: ToPin, options?: RemoveOptions & AbortOptions): Promise<CID>
      ls(options?: ListOptions & AbortOptions): AsyncIterable<PinEntry>
    }
    
    export type ToPin =
      | CID
      | string
      | ToPinWithPath
      | ToPinWithCID
    
    export type ToPinWithPath = {
      path: string
      recursive?: boolean
      metadata?: any
      cid?: undefined
    }
    
    export type ToPinWithCID = {
      cid?: CID
      recursive?: boolean
      metadata?: any
      path?: string
    }
    
    export type ToPins =
      | ToPin
      | Iterable<ToPin>
      | AsyncIterable<ToPin>
    
    export type Pin = {
      path: string | CID
      recursive: boolean
      metadata?: any
    }
    
    export type PinEntry = {
    cid: CID
    type: PinType
    }
    
    export type PinType =
      | 'direct'
      | 'recursive'
      | 'indirect'
    
    export type RemoveOptions = {
      /**
       * Recursively unpin the object linked
       */
      recursive?: boolean
    }
    
    export type AddOptions = {
      lock?: boolean
    }
    
    type ListOptions = {
      /**
       * CIDs or IPFS paths to search for in the pinset
       */
      paths?: Array<string|CID>
      /**
       * Filter by this type of pin ("recursive", "direct" or "indirect")
       */
      type?: PinType | 'all'
    }
  2. Each IPFS component that can provide @implements {API} annotation that type
    checker will ensure.

    Note: That does imply that implementer implements that exact API or it's
    superset which enables deriviation across ipfs-core and
    ipfs-http-client like added options in the later.

    Below is illustration of how ipfs-core can use this on the same pin
    component:

    /**
     * @implements {API}
     */
    class PinAPI {
      /**
       * @param {Object} config
       * @param {GCLock} config.gcLock
       * @param {DagReader} config.dagReader
       * @param {PinManager} config.pinManager
       */
      constructor ({ gcLock, dagReader, pinManager }) {
        // ...
      }
    }
    
    /**
     * @typedef {import('../../interface/pin').API} API
     */

    This will work regardless if we choose to do Incidental Complexity: Dependency injection is getting in the way #3391 or a slight variation on
    keep the current style used everywhere in chore: make IPFS API static (remove api-manager) #3365. If we keep
    the current style above will look as:

    const createAddAll = require('./add-all')
    
    /**
     * @implements {API}
     */
    class PinAPI {
      /**
       * @param {Object} config
       * @param {GCLock} config.gcLock
       * @param {DagReader} config.dagReader
       * @param {PinManager} config.pinManager
       */
      constructor ({ gcLock, dagReader, pinManager }) {
        this.addAll = createAddAll({ gcLock, dagReader, pinManager })
        // ...
      }
    }
    
    /**
     * @typedef {import('../../interface/pin').API} API
     */

    Where ./add-all.js looks like:

    /**
      * @param {Object} config
      * @param {GCLock} config.gcLock
      * @param {DagReader} config.dagReader
      * @param {PinManager} config.pinManager
      */
    module.exports = ({ pinManager, gcLock, dagReader }) => {
      /**
        * Adds multiple IPFS objects to the pinset and also stores it to the IPFS
        * repo. pinset is the set of hashes currently pinned (not gc'able)
        *
        * @param {ToPins} source - One or more CIDs or IPFS Paths to pin in your repo
        * @param {AddOptions & AbortOptions} [options]
        * @returns {AsyncIterable<CID>} - CIDs that were pinned.
        */
      async function * addAll (source, options = {}) {
        // ...
      }
    }
    
    /**
     * @typedef {import('../../interface/pin').API} API
     * @typedef {import('../../interface/pin').ToPins} ToPins
     * @typedef {import('../../interface/pin').AddOptions} AddOptions
     * @typedef {import('../../interface/util').AbortOptions} AbortOptions
     * ...
     */

    Note that all the types are imported from interface/*.

    And if we choose to reduce to remove dependency incejection (as per Incidental Complexity: Dependency injection is getting in the way #3391) things work just as well (better actually)

    /**
     * @implements {API}
     */
    class PinAPI {
      /**
       * @param {Object} config
       * @param {GCLock} config.gcLock
       * @param {DagReader} config.dagReader
       * @param {PinManager} config.pinManager
       */
      constructor ({ gcLock, dagReader, pinManager }) {
        // ...
      }
      /**
        * Adds multiple IPFS objects to the pinset and also stores it to the IPFS
        * repo. pinset is the set of hashes currently pinned (not gc'able)
        *
        * @param {ToPins} source - One or more CIDs or IPFS Paths to pin in your repo
        * @param {AddOptions & AbortOptions} [options]
        * @returns {AsyncIterable<CID>} - CIDs that were pinned.
        */
      addAll(source, options) {
        return addAll(this.state, source, options)
      }
      // ...
    }
  3. ipfs-http-client will do more or less the same, with the
    difference that config is whatever it is in ipfs-http-client:

    /**
     * @implements {API}
     */
    class PinAPI {
      /**
       * @param {Object} config
       * ....
       */
      constructor (config) {
        // ...
      }
      // ...
    }
    
    /**
     * @typedef {import('ipfs-core/src/interface/pin').API} API
     */

    Here as well we can continue with our current approach of
    dependency injections:

    const createAddAll = require('./add-all')
    
    /**
     * @implements {API}
     */
    class PinAPI {
      /**
       * @param {Object} config
       * ....
       */
      constructor (config) {
        this.addAll = createAddAll(config)
        // ...
      }
      // ...
    }
    
    /**
     * @typedef {import('ipfs-core/src/interface/pin').API} API
     */

    Or move to dependency injection free approach:

    const addAll = require('./add-all')
    
    /**
     * @implements {API}
     */
    class PinAPI {
      /**
       * @param {Object} config
       * ....
       */
      constructor (config) {
        // ...
      }
      /**
       * @param {ToPins} source
       * @param {AddOptions & ClientOptions} [options]
       */
      addAll (source, options) {
        return addAll(this.state, source, options)
      }
      // ...
    }
    
    /**
     * @typedef {import('ipfs-core/src/interface/pin').API} API
     * @typedef {import(ipfs-core/src/interface/pin').ToPins} ToPins
     * @typedef {import('ipfs-core/src/interface/pin').AddOptions} AddOptions
     * @typedef {import('../../util').HTTOptions} ClientOptions
     */

    Note: That implementation uses ClientOptions in place of AbortOptions (which it extends) while the class itself also implements pin API that type checker will ensure.

Additional notes

  • I believe @hugomrdias mentioned that in the future release of TS @typedef {import('ipfs-core/src/interface/pin')} Pin would gain namespace like behavior which will reduce amount of imports in the examples.
  • I expect that future improvements of TS to allow following patterns as well:
     import * from "./pin"
     export pin
     // ...
    Which will basically provide namespace like behavior so we could just have a single import.

Metadata

Metadata

Assignees

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions