ipfs-unixfs-importer
Version:
JavaScript implementation of the UnixFs importer used by IPFS
188 lines (148 loc) • 4.77 kB
text/typescript
import type { UnixFS } from 'ipfs-unixfs'
import batch from 'it-batch'
import type { CID } from 'multiformats/cid'
import type { InProgressImportResult } from '../index.js'
import type { FileLayout, Reducer } from '../layout/index.js'
const DEFAULT_LAYER_REPEAT = 4
const DEFAULT_MAX_CHILDREN_PER_NODE = 174
interface TrickleDagNode {
children: InProgressImportResult[]
depth: number
maxDepth: number
maxChildren: number
data?: InProgressImportResult[]
parent?: TrickleDagNode
cid?: CID
size?: number
unixfs?: UnixFS
}
export interface TrickleOptions {
layerRepeat?: number
maxChildrenPerNode?: number
}
/**
* @see https://github.com/ipfs/specs/pull/57#issuecomment-265205384
*/
export function trickle (options?: TrickleOptions): FileLayout {
const layerRepeat = options?.layerRepeat ?? DEFAULT_LAYER_REPEAT
const maxChildrenPerNode = options?.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE
return async function trickleLayout (source, reduce): Promise<InProgressImportResult> {
const root = new Root(layerRepeat)
let iteration = 0
let maxDepth = 1
let subTree: SubTree = root
for await (const layer of batch(source, maxChildrenPerNode)) {
if (subTree.isFull()) {
if (subTree !== root) {
root.addChild(await subTree.reduce(reduce))
}
if (iteration > 0 && iteration % layerRepeat === 0) {
maxDepth++
}
subTree = new SubTree(maxDepth, layerRepeat, iteration)
iteration++
}
subTree.append(layer)
}
if (subTree != null && subTree !== root) {
root.addChild(await subTree.reduce(reduce))
}
return await root.reduce(reduce)
}
}
class SubTree {
public root: TrickleDagNode
public node: TrickleDagNode
public parent: TrickleDagNode
public maxDepth: number
public layerRepeat: number
public currentDepth: number
public iteration: number
constructor (maxDepth: number, layerRepeat: number, iteration: number = 0) {
this.maxDepth = maxDepth
this.layerRepeat = layerRepeat
this.currentDepth = 1
this.iteration = iteration
this.root = this.node = this.parent = {
children: [],
depth: this.currentDepth,
maxDepth,
maxChildren: (this.maxDepth - this.currentDepth) * this.layerRepeat
}
}
isFull (): boolean {
if (this.root.data == null) {
return false
}
if (this.currentDepth < this.maxDepth && this.node.maxChildren > 0) {
// can descend
this._addNextNodeToParent(this.node)
return false
}
// try to find new node from node.parent
const distantRelative = this._findParent(this.node, this.currentDepth)
if (distantRelative != null) {
this._addNextNodeToParent(distantRelative)
return false
}
return true
}
_addNextNodeToParent (parent: TrickleDagNode): void {
this.parent = parent
// find site for new node
const nextNode = {
children: [],
depth: parent.depth + 1,
parent,
maxDepth: this.maxDepth,
maxChildren: Math.floor(parent.children.length / this.layerRepeat) * this.layerRepeat
}
// @ts-expect-error
parent.children.push(nextNode)
this.currentDepth = nextNode.depth
this.node = nextNode
}
append (layer: InProgressImportResult[]): void {
this.node.data = layer
}
async reduce (reduce: Reducer): Promise<InProgressImportResult> {
return await this._reduce(this.root, reduce)
}
async _reduce (node: TrickleDagNode, reduce: Reducer): Promise<InProgressImportResult> {
let children: InProgressImportResult[] = []
if (node.children.length > 0) {
children = await Promise.all(
node.children
// @ts-expect-error
.filter(child => child.data)
// @ts-expect-error
.map(async child => await this._reduce(child, reduce))
)
}
return await reduce((node.data ?? []).concat(children))
}
_findParent (node: TrickleDagNode, depth: number): TrickleDagNode | undefined {
const parent = node.parent
if (parent == null || parent.depth === 0) {
return
}
if (parent.children.length === parent.maxChildren || parent.maxChildren === 0) {
// this layer is full, may be able to traverse to a different branch
return this._findParent(parent, depth)
}
return parent
}
}
class Root extends SubTree {
constructor (layerRepeat: number) {
super(0, layerRepeat)
this.root.depth = 0
this.currentDepth = 1
}
addChild (child: InProgressImportResult): void {
this.root.children.push(child)
}
async reduce (reduce: Reducer): Promise<InProgressImportResult> {
return await reduce((this.root.data ?? []).concat(this.root.children))
}
}