Source

core/dagservice.ts

import Block from '@ipld/block'
import CID from 'cids'
import { getName } from 'multicodec'
import { BlockService } from './blockservice'

/**
 * AddOptions represents the set of default parameters to use when adding an IPLD Node via the DAG service.
 */
export interface AddOptions {
  /**
   * hashAlg is the hashing algorithm that is used to calculate the CID. The default comes from the given codec.
   */
  hashAlg?: string
  /**
   * onlyHash controls whether or not the serialized form of the IPLD Node will be passed to the underlying block store.
   */
  onlyHash?: boolean
  /**
   * cidVersion specifies which version of CID to use. This option is currently ignored (and defaults to v1).
   */
  cidVersion?: number
}

/**
 * DAGOptions represents the set of parameters to use when initializing a DAG service.
 */
export interface DAGOptions extends AddOptions {
  /**
   * The underlying block service for adding, deleting, and retrieving blocks. It is required.
   */
  blockService: BlockService
}

/**
 * ResolveResult represents an IPLD Node that was traversed during path resolving.
 */
export interface ResolveResult {
  /**
   * remainderPath is the part of the path that wasn’t resolved yet.
   */
  remainderPath?: string
  /**
   * value is what the resolved path points to. If further traversing is possible, then `value` is a CID object
   * linking to another IPLD node. If it was possible to fully resolve the path, `value` is the value the path points
   * to. So if you need the CID of the IPLD node you’re currently at, just take the `value` of the previously returned
   * IPLD node.
   */
  value: CID | any
}

/**
 * DAGService is a content-addressable store for adding, removing, and getting Merkle directed acyclic graphs (DAGs).
 * A DAG service provides an API for creating and resolving IPLD nodes, making it possible for apps and services to
 * integrate with the IPFS IPLD Merkle-forest.
 *
 * It satisfies the default IPLD interface: https://github.com/ipld/js-ipld.
 */
export class DAGService {
  /**
   * A set of default parameters to use when adding an IPLD Node via the DAG service.
   */
  public defaultOptions: AddOptions
  /**
   * The underlying block service for adding, deleting, and retrieving blocks.
   */
  public blockService: BlockService

  /**
   * DAGService creates a new directed acyclic graph (DAG) service.
   *
   * @param {DAGOptions} options The set of parameters to use when initializing a DAG service. Must include a `blockService` entry.
   */
  constructor(options: DAGOptions) {
    const { blockService, ...opts } = options
    this.blockService = blockService
    this.defaultOptions = opts
  }

  /**
   * resolve retrieves IPLD nodes along the path that is rooted at a given IPLD node.
   *
   * @param {CID} cid The content identifier at which to start resolving.
   * @param {string} path The path that should be resolved.
   */
  async *resolve(cid: CID, path: string): AsyncIterableIterator<ResolveResult> {
    const data = await this.blockService.get(cid)
    const block = Block.create(data.data, data.cid)
    const reader = block.reader()
    const node = reader.get(path)
    yield { value: node.value, remainderPath: node.remaining || '' }
    if (CID.isCID(node.value)) {
      for await (const res of this.resolve(node.value, node.remaining || '')) {
        yield res
      }
    }
  }

  /**
   * tree returns all the paths that can be resolved into.
   *
   * @param {CID} cid The ID to get the paths from
   * @param {string} offsetPath the path to start to retrieve the other paths from.
   * @param {object} options Currently only whether to get the paths recursively or not. If `recursive` is
   * `false`, it will only resolve the paths of the given CID.
   */
  async *tree(cid: CID, offsetPath = '', options = { recursive: false }): AsyncIterableIterator<string> {
    const data = await this.blockService.get(cid)
    const block = Block.create(data.data, data.cid)
    const reader = block.reader()
    // Start with this block and its sub-paths
    for await (let path of reader.tree()) {
      // Return it if it matches the given offset path, but is not the offset path itself
      if (path.startsWith(offsetPath) && path.length > offsetPath.length) {
        if (offsetPath.length > 0) {
          path = path.slice(offsetPath.length + 1)
        }
        yield path
      }
    }
    // Finish with links if recursive
    if (options.recursive) {
      for await (const [path, sub] of reader.links()) {
        for await (const subPath of this.tree(sub, undefined, options)) {
          let fullPath = `${path}/${subPath}`
          if (fullPath.startsWith(offsetPath) && fullPath.length > offsetPath.length) {
            if (offsetPath.length > 0) {
              fullPath = fullPath.slice(offsetPath.length + 1)
            }
            yield fullPath
          }
        }
      }
    }
  }

  /**
   * get returns an IPLD node by its content identifier.
   *
   * @param {CID} cid The content identifier for the IPLD node that should be retrieved.
   */
  async get(cid: CID) {
    const data = await this.blockService.get(cid)
    const block = Block.create(data.data, data.cid)
    return block.decode()
  }

  /**
   * getMany returns multiple IPLD nodes from an iterable of content identifiers.
   *
   * @param {Iterable<CID>} cids The content identifiers for the IPLD nodes that should be retrieved.
   */
  async *getMany(cids: Iterable<CID>): AsyncIterableIterator<any> {
    for await (const cid of cids) {
      yield this.get(cid)
    }
  }

  /**
   * put encodes and adds an IPLD node using the specified codec.
   *
   * @param {any} node The deserialized IPLD node that should be added. Can be any value, object, Buffer, JSON, etc.
   * @param {string | number} codec A string representing the multicodec with which the IPLD node should be encoded.
   * Can also be a numeric multicodec code for compatibility with js-ipld.
   * @param {AddOptions} options The specific parameters to use when encoding the input node.
   */
  async put(node: any, codec: string | number = 'dag-cbor', options?: AddOptions) {
    const opts = options || this.defaultOptions || {}
    if (typeof codec === 'number') {
      // Compatibility with js-ipld
      codec = getName(codec)
    }
    const block = Block.encoder(node, codec, opts.hashAlg)
    const data = block.encode()
    let cid = await block.cid()
    if (opts.cidVersion === 0) {
      // Compatibility with js-ipld
      cid = cid.toV0()
    }
    if (!opts.onlyHash) {
      await this.blockService.put({ data, cid })
    }
    return cid
  }

  /**
   * putMany encodes and adds multiple IPLD nodes using the specified codec.
   *
   * @param {Iterable<any>} nodes The deserialized IPLD nodes that should be added.
   * @param {string | number} codec A string representing the multicodec with which the IPLD node should be encoded.
   * Can also be a numeric multicodec code for compatibility with js-ipld.
   * @param {AddOptions} options The specific parameters to use when encoding the input node.
   */
  async *putMany(
    nodes: Iterable<any>,
    codec: string | number = 'dag-cbor',
    options?: AddOptions,
  ): AsyncIterableIterator<CID> {
    const opts = options || this.defaultOptions || {}
    if (typeof codec === 'number') {
      codec = getName(codec) // Compatibility with js-ipld
    }
    for await (const node of nodes) {
      yield this.put(node, codec, opts)
    }
  }

  /**
   * remove deletes an IPLD node from the local store.
   *
   * @param {CID} cid The content identifier for the IPLD node to be removed.
   */
  async remove(cid: CID) {
    return this.blockService.delete(cid)
  }

  /**
   * removeMany deletes multiple IPLD nodes from the local store.
   *
   * Throws an error if any of the Blocks can’t be removed. This operation is *not* atomic, some nodes might have
   * already been removed.
   *
   * @param {Iterable<CID>} cids The content identifiers for the IPLD nodes to be removed.
   */
  async *removeMany(cids: Iterable<CID>): AsyncIterableIterator<CID> {
    for await (const cid of cids) {
      yield this.remove(cid)
    }
  }
}