diff --git a/packages/car/src/export-strategies/subgraph-exporter.ts b/packages/car/src/export-strategies/subgraph-exporter.ts index e7009a9a8..383107abe 100644 --- a/packages/car/src/export-strategies/subgraph-exporter.ts +++ b/packages/car/src/export-strategies/subgraph-exporter.ts @@ -1,11 +1,19 @@ import { breadthFirstWalker } from '@helia/utils' import type { ExportStrategy } from '../index.js' import type { CodecLoader } from '@helia/interface' +import type { GraphWalker, GraphWalkerComponents } from '@helia/utils' import type { AbortOptions } from '@libp2p/interface' import type { Blockstore } from 'interface-blockstore' import type { BlockView } from 'multiformats' import type { CID } from 'multiformats/cid' +export interface SubgraphExporterInit { + /** + * Graph traversal strategy, defaults to breadth-first + */ + walker?(components: GraphWalkerComponents): GraphWalker +} + /** * Traverses the DAG breadth-first starting at the target CID and yields all * encountered blocks. @@ -14,11 +22,24 @@ import type { CID } from 'multiformats/cid' * the helia config. */ export class SubgraphExporter implements ExportStrategy { + private walker?: (components: GraphWalkerComponents) => GraphWalker + + constructor (init?: SubgraphExporterInit) { + this.walker = init?.walker + } + async * export (cid: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator, void, undefined> { - const walker = breadthFirstWalker({ + let walker: GraphWalker + const components = { blockstore, getCodec - }) + } + + if (this.walker != null) { + walker = this.walker(components) + } else { + walker = breadthFirstWalker()(components) + } for await (const node of walker.walk(cid, options)) { yield node.block diff --git a/packages/car/src/export-strategies/unixfs-exporter.ts b/packages/car/src/export-strategies/unixfs-exporter.ts index e46dda71c..b0917a3a8 100644 --- a/packages/car/src/export-strategies/unixfs-exporter.ts +++ b/packages/car/src/export-strategies/unixfs-exporter.ts @@ -83,7 +83,7 @@ export class UnixFSExporter implements ExportStrategy { throw new NotUnixFSError('Target CID was not UnixFS - use the SubGraphExporter to export arbitrary graphs') } - const walker = depthFirstWalker({ + const walker = depthFirstWalker()({ blockstore, getCodec }) diff --git a/packages/car/src/index.ts b/packages/car/src/index.ts index ae30fccc5..4c359a550 100644 --- a/packages/car/src/index.ts +++ b/packages/car/src/index.ts @@ -171,6 +171,10 @@ export interface ExportStrategy { export * from './export-strategies/index.js' export * from './traversal-strategies/index.js' +// re-export walkers from @helia/utils so consumers don't need an extra dep +export type { GraphWalker } from '@helia/utils' +export { depthFirstWalker, breadthFirstWalker, naturalOrderWalker } from '@helia/utils' + export interface ExportCarOptions extends AbortOptions, ProgressOptions, ProviderOptions { /** * If true, the blockstore will not do any network requests. diff --git a/packages/car/src/traversal-strategies/graph-search.ts b/packages/car/src/traversal-strategies/graph-search.ts index f5adf9915..b64e05c79 100644 --- a/packages/car/src/traversal-strategies/graph-search.ts +++ b/packages/car/src/traversal-strategies/graph-search.ts @@ -5,13 +5,23 @@ import { createUnsafe } from 'multiformats/block' import { InvalidTraversalError } from '../errors.ts' import type { TraversalStrategy } from '../index.js' import type { CodecLoader } from '@helia/interface' -import type { GraphWalker } from '@helia/utils' +import type { GraphWalker, GraphWalkerComponents } from '@helia/utils' import type { AbortOptions } from '@libp2p/interface' import type { Blockstore } from 'interface-blockstore' import type { BlockView } from 'multiformats' import type { CID } from 'multiformats/cid' export interface GraphSearchOptions { + /** + * Graph traversal strategy, defaults to breadth-first + */ + walker?(components: GraphWalkerComponents): GraphWalker + + /** + * How to search the graph + * + * @deprecated use `walker` instead - this will be removed in a future release + */ strategy?: 'depth-first' | 'breadth-first' } @@ -27,6 +37,7 @@ export class GraphSearch implements TraversalStrategy { private haystack?: CID private readonly needle: CID private readonly options?: GraphSearchOptions + private walker?: (components: GraphWalkerComponents) => GraphWalker constructor (needle: CID, options?: GraphSearchOptions) constructor (haystack: CID, needle: CID, options?: GraphSearchOptions) @@ -43,22 +54,24 @@ export class GraphSearch implements TraversalStrategy { } else { throw new InvalidParametersError('needle must be specified') } + + this.walker = this.options?.walker } async * traverse (root: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator, void, undefined> { const start = this.haystack ?? root let walker: GraphWalker + const components = { + blockstore, + getCodec + } - if (this.options?.strategy === 'breadth-first') { - walker = breadthFirstWalker({ - blockstore, - getCodec - }) + if (this.walker != null) { + walker = this.walker(components) + } else if (this.options?.strategy === 'breadth-first') { + walker = breadthFirstWalker()(components) } else { - walker = depthFirstWalker({ - blockstore, - getCodec - }) + walker = depthFirstWalker()(components) } for await (const node of walker.walk(start, options)) { diff --git a/packages/car/test/export.spec.ts b/packages/car/test/export.spec.ts index 0b6c4e67a..fd3a62ec7 100644 --- a/packages/car/test/export.spec.ts +++ b/packages/car/test/export.spec.ts @@ -1,5 +1,6 @@ /* eslint-env mocha */ +import { unixfs } from '@helia/unixfs' import { CarReader } from '@ipld/car' import * as dagCbor from '@ipld/dag-cbor' import { defaultLogger } from '@libp2p/logger' @@ -15,7 +16,7 @@ import { CID } from 'multiformats/cid' import { sha256 } from 'multiformats/hashes/sha2' import sinon from 'sinon' import { BlockExporter, SubgraphExporter, UnixFSExporter } from '../src/export-strategies/index.js' -import { car } from '../src/index.js' +import { breadthFirstWalker, depthFirstWalker, car } from '../src/index.js' import { CIDPath, GraphSearch, UnixFSPath } from '../src/traversal-strategies/index.js' import { carEquals, CarEqualsSkip } from './fixtures/car-equals.js' import { getCodec } from './fixtures/get-codec.js' @@ -28,6 +29,12 @@ describe('export', () => { let c: Car let blockstoreGetSpy: sinon.SinonSpy + // contains a DAG-CBOR block + const nonUnixFsRoot = CID.parse('bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei') + + // contains a HAMT shard root block + const shardRoot = CID.parse('bafybeidbclfqleg2uojchspzd4bob56dqetqjsj27gy2cq3klkkgxtpn4i') + // "/" (1x block) const dagRootCid = CID.parse('bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu') @@ -63,8 +70,7 @@ describe('export', () => { }) it('should round-trip fixture CAR file', async () => { - // cspell:ignore bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu - const { reader, bytes } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader, bytes } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) // import all the blocks from the car file await c.import(reader) @@ -79,7 +85,7 @@ describe('export', () => { }) it('should export a single block from a DAG', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -104,9 +110,7 @@ describe('export', () => { it('should error on non-UnixFS data with UnixFSExporter', async () => { // dag-cbor only blocks in this car file - const nonUnixFsRoot = CID.parse('bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei') - // cspell:ignore bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei - const { reader } = await loadCarFixture('test/fixtures/bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei.car') + const { reader } = await loadCarFixture(`test/fixtures/${nonUnixFsRoot}.car`) await c.import(reader) @@ -117,9 +121,7 @@ describe('export', () => { it('should export non-UnixFS data with SubGraphExporter', async () => { // dag-cbor only blocks in this car file - const nonUnixFsRoot = CID.parse('bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei') - // cspell:ignore bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei - const { reader } = await loadCarFixture('test/fixtures/bafyreieurv3eg6sxth6avdr2zel52mdcqw7dghkljzcnaodb4conrzqjei.car') + const { reader } = await loadCarFixture(`test/fixtures/${nonUnixFsRoot}.car`) await c.import(reader) @@ -143,8 +145,98 @@ describe('export', () => { expect(blockstoreGetSpy.callCount).to.equal(1) }) + it('should default to breadth-first with SubGraphExporter', async () => { + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) + + await c.import(reader) + + // create a directory like `subdirCid` but with a file after multiblock.txt + // so we can assert traversal is correct + const u = unixfs({ blockstore }) + const dirCid = await u.cp(asciiTextCid, subdirCid, 'qux.txt') + + const ourReader = await CarReader.fromBytes(await toBuffer(c.export(dirCid, { + exporter: new SubgraphExporter() + }))) + + await expect(ourReader.getRoots()).to.eventually.deep.equal([ + dirCid + ]) + + // should traverse 'qux.txt' before descending into 'multiblock.txt' + await expect(all(ourReader.cids())).to.eventually.deep.equal([ + dirCid, + asciiTextCid, + helloTextCid, + multiBlockTxtCid, + asciiTextCid, + ...multiBlockTxtCids + ]) + }) + + it('should export depth-first with SubGraphExporter', async () => { + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) + + await c.import(reader) + + // create a directory like `subdirCid` but with a file after multiblock.txt + // so we can assert traversal is correct + const u = unixfs({ blockstore }) + const dirCid = await u.cp(asciiTextCid, subdirCid, 'qux.txt') + + const ourReader = await CarReader.fromBytes(await toBuffer(c.export(dirCid, { + exporter: new SubgraphExporter({ + walker: depthFirstWalker() + }) + }))) + + await expect(ourReader.getRoots()).to.eventually.deep.equal([ + dirCid + ]) + // should descend into 'multiblock.txt' before 'qux.txt' + await expect(all(ourReader.cids())).to.eventually.deep.equal([ + dirCid, + asciiTextCid, + helloTextCid, + multiBlockTxtCid, + ...multiBlockTxtCids, + asciiTextCid + ]) + }) + + it('should export breadth-first with SubGraphExporter', async () => { + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) + + await c.import(reader) + + // create a directory like `subdirCid` but with a file after multiblock.txt + // so we can assert traversal is correct + const u = unixfs({ blockstore }) + const dirCid = await u.cp(asciiTextCid, subdirCid, 'qux.txt') + + const ourReader = await CarReader.fromBytes(await toBuffer(c.export(dirCid, { + exporter: new SubgraphExporter({ + walker: breadthFirstWalker() + }) + }))) + + await expect(ourReader.getRoots()).to.eventually.deep.equal([ + dirCid + ]) + + // should traverse 'qux.txt' before descending into 'multiblock.txt' + await expect(all(ourReader.cids())).to.eventually.deep.equal([ + dirCid, + asciiTextCid, + helloTextCid, + multiBlockTxtCid, + asciiTextCid, + ...multiBlockTxtCids + ]) + }) + it('should only include root block when block exporter is used', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -195,7 +287,7 @@ describe('export', () => { describe('unixfs-exporter', () => { it('should export the start of a file', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -216,7 +308,7 @@ describe('export', () => { }) it('should export a slice of a file', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -237,7 +329,7 @@ describe('export', () => { }) it('should require a positive offset', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -251,7 +343,7 @@ describe('export', () => { }) it('should require a positive length', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -265,7 +357,7 @@ describe('export', () => { }) it('should export a slice of a file when later blocks are missing', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -289,7 +381,7 @@ describe('export', () => { }) it('should export a slice of a file when early blocks are missing', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -313,9 +405,6 @@ describe('export', () => { }) it('should export a hamt-sharded directory listing', async () => { - // cspell:ignore bafybeidbclfqleg2uojchspzd4bob56dqetqjsj27gy2cq3klkkgxtpn4i - const shardRoot = CID.parse('bafybeidbclfqleg2uojchspzd4bob56dqetqjsj27gy2cq3klkkgxtpn4i') - const { reader, bytes: carFileBytes } = await loadCarFixture(`test/fixtures/${shardRoot}.car`) // import all the blocks from the car file @@ -340,8 +429,7 @@ describe('export', () => { describe('graph-search', () => { it('should find a sub-DAG using a CID and export it', async () => { - // cspell:ignore bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) // import all the blocks from the car file await c.import(reader) @@ -363,8 +451,7 @@ describe('export', () => { }) it('should find a sub-DAG using a CID and export it starting from a parent node', async () => { - // cspell:ignore bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) // import all the blocks from the car file await c.import(reader) @@ -389,7 +476,7 @@ describe('export', () => { describe('cid-path', () => { it('should use CIDPath to restrain DAG traversal', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -415,7 +502,7 @@ describe('export', () => { }) it('should not include traversal blocks when traversing from a parent node and includeTraversalBlocks is not set', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -436,7 +523,7 @@ describe('export', () => { }) it('should include traversal blocks when traversing from a parent node and includeTraversalBlocks is set', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -460,7 +547,7 @@ describe('export', () => { }) it('should throw if CID path traversal does not lead to export root', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -472,7 +559,7 @@ describe('export', () => { }) it('should throw an error when an invalid path is provided to CID path traversal', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) @@ -491,8 +578,7 @@ describe('export', () => { describe('unixfs-path', () => { it('should find a sub-DAG using a path and export it', async () => { - // cspell:ignore bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) // import all the blocks from the car file await c.import(reader) @@ -513,8 +599,7 @@ describe('export', () => { }) it('should export part of a DAG', async () => { - // cspell:ignore bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) // import all the blocks from the car file await c.import(reader) @@ -536,7 +621,7 @@ describe('export', () => { }) it('should use UnixFSPath to restrain DAG traversal', async () => { - const { reader } = await loadCarFixture('test/fixtures/bafybeidh6k2vzukelqtrjsmd4p52cpmltd2ufqrdtdg6yigi73in672fwu.car') + const { reader } = await loadCarFixture(`test/fixtures/${dagRootCid}.car`) await c.import(reader) diff --git a/packages/utils/src/graph-walker.ts b/packages/utils/src/graph-walker.ts index d78bf131f..827350322 100644 --- a/packages/utils/src/graph-walker.ts +++ b/packages/utils/src/graph-walker.ts @@ -26,12 +26,27 @@ export interface GraphWalker { walk (cid: CID, options?: WalkOptions): AsyncGenerator> } -export function depthFirstWalker (components: GraphWalkerComponents, init: GraphWalkerInit = {}): GraphWalker { - return new DepthFirstGraphWalker(components, init) +/** + * A depth-first walker descends into child blocks before processing successor + * sibling blocks + */ +export function depthFirstWalker (init?: GraphWalkerInit): (components: GraphWalkerComponents) => GraphWalker { + return (components) => new DepthFirstGraphWalker(components, init) } -export function breadthFirstWalker (components: GraphWalkerComponents, init: GraphWalkerInit = {}): GraphWalker { - return new BreadthFirstGraphWalker(components, init) +/** + * A breadth-first walker processes sibling blocks before child blocks + */ +export function breadthFirstWalker (init?: GraphWalkerInit): (components: GraphWalkerComponents) => GraphWalker { + return (components) => new BreadthFirstGraphWalker(components, init) +} + +/** + * A natural order walker processes blocks in the order defined by the codec and + * does not try to sort them + */ +export function naturalOrderWalker (init?: GraphWalkerInit): (components: GraphWalkerComponents) => GraphWalker { + return (components) => new NaturalOrderGraphWalker(components, init) } interface JobOptions extends AbortOptions { @@ -47,7 +62,7 @@ export interface WalkOptions extends AbortOptions { abstract class AbstractGraphWalker { private readonly components: GraphWalkerComponents - constructor (components: GraphWalkerComponents, init: GraphWalkerInit) { + constructor (components: GraphWalkerComponents, init: GraphWalkerInit = {}) { this.components = components } @@ -156,3 +171,11 @@ class BreadthFirstGraphWalker extends AbstractGraphWalker { }) } } + +export class NaturalOrderGraphWalker extends AbstractGraphWalker { + getQueue(): Queue | undefined, JobOptions> { + return new Queue | undefined, JobOptions>({ + concurrency: 1 + }) + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1f56db42c..77d7ec38f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -36,7 +36,7 @@ export type { AbstractCreateSessionOptions, BlockstoreSessionEvents, AbstractSes export type { BlockStorage, BlockStorageInit } -export { breadthFirstWalker, depthFirstWalker } from './graph-walker.ts' +export { breadthFirstWalker, depthFirstWalker, naturalOrderWalker } from './graph-walker.ts' export type { GraphWalkerComponents, GraphWalkerInit, GraphNode, GraphWalker } from './graph-walker.ts' /** diff --git a/packages/utils/test/graph-walker.spec.ts b/packages/utils/test/graph-walker.spec.ts index 7aaaa208e..afbfe4233 100644 --- a/packages/utils/test/graph-walker.spec.ts +++ b/packages/utils/test/graph-walker.spec.ts @@ -5,7 +5,7 @@ import all from 'it-all' import map from 'it-map' import { CID } from 'multiformats/cid' import { sha256 } from 'multiformats/hashes/sha2' -import { breadthFirstWalker, depthFirstWalker } from '../src/graph-walker.ts' +import { breadthFirstWalker, depthFirstWalker, naturalOrderWalker } from '../src/graph-walker.ts' import type { CodecLoader } from '@helia/interface' import type { Blockstore } from 'interface-blockstore' @@ -105,7 +105,7 @@ describe('graph-walker', () => { describe('depth-first', () => { it('should walk depth-first', async () => { - const walker = depthFirstWalker({ + const walker = depthFirstWalker()({ blockstore, getCodec }) @@ -122,7 +122,7 @@ describe('graph-walker', () => { }) it('should filter children', async () => { - const walker = depthFirstWalker({ + const walker = depthFirstWalker()({ blockstore, getCodec }) @@ -145,7 +145,7 @@ describe('graph-walker', () => { describe('breadth-first', () => { it('should walk breadth-first', async () => { - const walker = breadthFirstWalker({ + const walker = breadthFirstWalker()({ blockstore, getCodec }) @@ -162,7 +162,47 @@ describe('graph-walker', () => { }) it('should filter children', async () => { - const walker = breadthFirstWalker({ + const walker = breadthFirstWalker()({ + blockstore, + getCodec + }) + + const result = await all(map(walker.walk(nodes.root.cid, { + includeChild (child, parent) { + return parent.value.name === 'root' || parent.value.name === 'a' + } + }), (node) => { + const obj = dagCbor.decode(node.block.bytes) + + return obj.name + })) + + expect(result).to.deep.equal([ + 'root', 'a', 'b', 'c', 'd', 'e', 'f' + ]) + }) + }) + + describe('natural-order', () => { + it('should walk depth-first', async () => { + const walker = naturalOrderWalker()({ + blockstore, + getCodec + }) + + const result = await all(map(walker.walk(nodes.root.cid), (node) => { + const obj = dagCbor.decode(node.block.bytes) + + return obj.name + })) + + expect(result).to.deep.equal([ + 'root', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l' + ]) + }) + + it('should filter children', async () => { + const walker = naturalOrderWalker()({ blockstore, getCodec })