Skip to content

Commit 7676912

Browse files
authored
feat: allow specifying graph export strategy (#907)
Support BFS, DFS or codec-specific graph traversal as an option.
1 parent e8ebe26 commit 7676912

File tree

8 files changed

+244
-58
lines changed

8 files changed

+244
-58
lines changed

packages/car/src/export-strategies/subgraph-exporter.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { breadthFirstWalker } from '@helia/utils'
22
import type { ExportStrategy } from '../index.js'
33
import type { CodecLoader } from '@helia/interface'
4+
import type { GraphWalker, GraphWalkerComponents } from '@helia/utils'
45
import type { AbortOptions } from '@libp2p/interface'
56
import type { Blockstore } from 'interface-blockstore'
67
import type { BlockView } from 'multiformats'
78
import type { CID } from 'multiformats/cid'
89

10+
export interface SubgraphExporterInit {
11+
/**
12+
* Graph traversal strategy, defaults to breadth-first
13+
*/
14+
walker?(components: GraphWalkerComponents): GraphWalker
15+
}
16+
917
/**
1018
* Traverses the DAG breadth-first starting at the target CID and yields all
1119
* encountered blocks.
@@ -14,11 +22,24 @@ import type { CID } from 'multiformats/cid'
1422
* the helia config.
1523
*/
1624
export class SubgraphExporter implements ExportStrategy {
25+
private walker?: (components: GraphWalkerComponents) => GraphWalker
26+
27+
constructor (init?: SubgraphExporterInit) {
28+
this.walker = init?.walker
29+
}
30+
1731
async * export (cid: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined> {
18-
const walker = breadthFirstWalker({
32+
let walker: GraphWalker
33+
const components = {
1934
blockstore,
2035
getCodec
21-
})
36+
}
37+
38+
if (this.walker != null) {
39+
walker = this.walker(components)
40+
} else {
41+
walker = breadthFirstWalker()(components)
42+
}
2243

2344
for await (const node of walker.walk(cid, options)) {
2445
yield node.block

packages/car/src/export-strategies/unixfs-exporter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class UnixFSExporter implements ExportStrategy {
8383
throw new NotUnixFSError('Target CID was not UnixFS - use the SubGraphExporter to export arbitrary graphs')
8484
}
8585

86-
const walker = depthFirstWalker({
86+
const walker = depthFirstWalker()({
8787
blockstore,
8888
getCodec
8989
})

packages/car/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ export interface ExportStrategy {
171171
export * from './export-strategies/index.js'
172172
export * from './traversal-strategies/index.js'
173173

174+
// re-export walkers from @helia/utils so consumers don't need an extra dep
175+
export type { GraphWalker } from '@helia/utils'
176+
export { depthFirstWalker, breadthFirstWalker, naturalOrderWalker } from '@helia/utils'
177+
174178
export interface ExportCarOptions extends AbortOptions, ProgressOptions<GetBlockProgressEvents>, ProviderOptions {
175179
/**
176180
* If true, the blockstore will not do any network requests.

packages/car/src/traversal-strategies/graph-search.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,23 @@ import { createUnsafe } from 'multiformats/block'
55
import { InvalidTraversalError } from '../errors.ts'
66
import type { TraversalStrategy } from '../index.js'
77
import type { CodecLoader } from '@helia/interface'
8-
import type { GraphWalker } from '@helia/utils'
8+
import type { GraphWalker, GraphWalkerComponents } from '@helia/utils'
99
import type { AbortOptions } from '@libp2p/interface'
1010
import type { Blockstore } from 'interface-blockstore'
1111
import type { BlockView } from 'multiformats'
1212
import type { CID } from 'multiformats/cid'
1313

1414
export interface GraphSearchOptions {
15+
/**
16+
* Graph traversal strategy, defaults to breadth-first
17+
*/
18+
walker?(components: GraphWalkerComponents): GraphWalker
19+
20+
/**
21+
* How to search the graph
22+
*
23+
* @deprecated use `walker` instead - this will be removed in a future release
24+
*/
1525
strategy?: 'depth-first' | 'breadth-first'
1626
}
1727

@@ -27,6 +37,7 @@ export class GraphSearch implements TraversalStrategy {
2737
private haystack?: CID
2838
private readonly needle: CID
2939
private readonly options?: GraphSearchOptions
40+
private walker?: (components: GraphWalkerComponents) => GraphWalker
3041

3142
constructor (needle: CID, options?: GraphSearchOptions)
3243
constructor (haystack: CID, needle: CID, options?: GraphSearchOptions)
@@ -43,22 +54,24 @@ export class GraphSearch implements TraversalStrategy {
4354
} else {
4455
throw new InvalidParametersError('needle must be specified')
4556
}
57+
58+
this.walker = this.options?.walker
4659
}
4760

4861
async * traverse (root: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined> {
4962
const start = this.haystack ?? root
5063
let walker: GraphWalker
64+
const components = {
65+
blockstore,
66+
getCodec
67+
}
5168

52-
if (this.options?.strategy === 'breadth-first') {
53-
walker = breadthFirstWalker({
54-
blockstore,
55-
getCodec
56-
})
69+
if (this.walker != null) {
70+
walker = this.walker(components)
71+
} else if (this.options?.strategy === 'breadth-first') {
72+
walker = breadthFirstWalker()(components)
5773
} else {
58-
walker = depthFirstWalker({
59-
blockstore,
60-
getCodec
61-
})
74+
walker = depthFirstWalker()(components)
6275
}
6376

6477
for await (const node of walker.walk(start, options)) {

0 commit comments

Comments
 (0)