-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes: #118 Basic traversal functionality for deterministic DAG walking with no repeat block visits and support for block skipping. User supplies a block loader, which can be used to watch the block ordering of the walk.
- Loading branch information
Showing
4 changed files
with
241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { base58btc } from 'multiformats/bases/base58' | ||
|
||
/** | ||
* @typedef {import('./cid.js').CID} CID | ||
*/ | ||
|
||
/** | ||
* @template T | ||
* @typedef {import('./block.js').Block<T>} Block | ||
*/ | ||
|
||
/** | ||
* @template T | ||
* @param {Object} options | ||
* @param {CID} options.cid | ||
* @param {(cid: CID) => Promise<Block<T>|null>} options.load | ||
* @param {Set<string>?} options.seen | ||
*/ | ||
const walk = async ({ cid, load, seen }) => { | ||
seen = seen || new Set() | ||
const b58Cid = cid.toString(base58btc) | ||
if (seen.has(b58Cid)) { | ||
return | ||
} | ||
|
||
const block = await load(cid) | ||
seen.add(b58Cid) | ||
|
||
if (block === null) { // the loader signals with `null` that we should skip this block | ||
return | ||
} | ||
|
||
for (const [, cid] of block.links()) { | ||
await walk({ cid, load, seen }) | ||
} | ||
} | ||
|
||
export { walk } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* globals describe, it */ | ||
import * as codec from 'multiformats/codecs/json' | ||
import * as dagPB from '@ipld/dag-pb' | ||
import { sha256 as hasher } from 'multiformats/hashes/sha2' | ||
import * as main from 'multiformats/block' | ||
import { walk } from 'multiformats/traversal' | ||
import { deepStrictEqual as same } from 'assert' | ||
|
||
const test = it | ||
const { createLink, createNode } = dagPB | ||
|
||
describe('traversal', () => { | ||
describe('walk', async () => { | ||
// Forming the following DAG for testing | ||
// A | ||
// / \ | ||
// B C | ||
// / \ / \ | ||
// D D D E | ||
const linksE = [] | ||
const valueE = createNode(Uint8Array.from('string E qacdswa'), linksE) | ||
const blockE = await main.encode({ value: valueE, codec, hasher }) | ||
const cidE = blockE.cid | ||
|
||
const linksD = [] | ||
const valueD = createNode(Uint8Array.from('string D zasa'), linksD) | ||
const blockD = await main.encode({ value: valueD, codec, hasher }) | ||
const cidD = blockD.cid | ||
|
||
const linksC = [createLink('link1', 100, cidD), createLink('link2', 100, cidE)] | ||
const valueC = createNode(Uint8Array.from('string C zxc'), linksC) | ||
const blockC = await main.encode({ value: valueC, codec, hasher }) | ||
const cidC = blockC.cid | ||
|
||
const linksB = [createLink('link1', 100, cidD), createLink('link2', 100, cidD)] | ||
const valueB = createNode(Uint8Array.from('string B lpokjiasd'), linksB) | ||
const blockB = await main.encode({ value: valueB, codec, hasher }) | ||
const cidB = blockB.cid | ||
|
||
const linksA = [createLink('link1', 100, cidB), createLink('link2', 100, cidC)] | ||
const valueA = createNode(Uint8Array.from('string A qwertcfdgshaa'), linksA) | ||
const blockA = await main.encode({ value: valueA, codec, hasher }) | ||
const cidA = blockA.cid | ||
|
||
const load = async (cid) => { | ||
if (cid.equals(cidE)) { | ||
return blockE | ||
} | ||
if (cid.equals(cidD)) { | ||
return blockD | ||
} | ||
if (cid.equals(cidC)) { | ||
return blockC | ||
} | ||
if (cid.equals(cidB)) { | ||
return blockB | ||
} | ||
if (cid.equals(cidA)) { | ||
return blockA | ||
} | ||
return null | ||
} | ||
|
||
const loadWrapper = (load, arr = []) => (cid) => { | ||
arr.push(cid.toString()) | ||
return load(cid) | ||
} | ||
|
||
test('block with no links', async () => { | ||
// Test Case 1 | ||
// Input DAG | ||
// D | ||
// | ||
// Expect load to be called with D | ||
const expectedCallArray = [cidD.toString()] | ||
const callArray = [] | ||
|
||
await walk({ cid: cidD, load: loadWrapper(load, callArray) }) | ||
|
||
expectedCallArray.forEach((value, index) => { | ||
same(value, callArray[index]) | ||
}) | ||
}) | ||
|
||
test('block with links', async () => { | ||
// Test Case 2 | ||
// Input | ||
// C | ||
// / \ | ||
// D E | ||
// | ||
// Expect load to be called with C, then D, then E | ||
const expectedCallArray = [cidC.toString(), cidD.toString(), cidE.toString()] | ||
const callArray = [] | ||
|
||
await walk({ cid: cidC, load: loadWrapper(load, callArray) }) | ||
|
||
expectedCallArray.forEach((value, index) => { | ||
same(value, callArray[index]) | ||
}) | ||
}) | ||
|
||
test('block with matching links', async () => { | ||
// Test Case 3 | ||
// Input | ||
// B | ||
// / \ | ||
// D D | ||
// | ||
// Expect load to be called with B, then D | ||
const expectedCallArray = [cidB.toString(), cidD.toString()] | ||
const callArray = [] | ||
|
||
await walk({ cid: cidB, load: loadWrapper(load, callArray) }) | ||
|
||
expectedCallArray.forEach((value, index) => { | ||
same(value, callArray[index]) | ||
}) | ||
}) | ||
|
||
test('depth first with duplicated block', async () => { | ||
// Test Case 4 | ||
// Input | ||
// A | ||
// / \ | ||
// B C | ||
// / \ / \ | ||
// D D D E | ||
// | ||
// Expect load to be called with A, then B, then D, then C, then E | ||
const expectedCallArray = [ | ||
cidA.toString(), | ||
cidB.toString(), | ||
cidD.toString(), | ||
cidC.toString(), | ||
cidE.toString() | ||
] | ||
const callArray = [] | ||
|
||
await walk({ cid: cidA, load: loadWrapper(load, callArray) }) | ||
|
||
expectedCallArray.forEach((value, index) => { | ||
same(value, callArray[index]) | ||
}) | ||
}) | ||
|
||
test('null return', async () => { | ||
const links = [] | ||
const value = createNode(Uint8Array.from('test'), links) | ||
const block = await main.encode({ value: value, codec, hasher }) | ||
const cid = block.cid | ||
const expectedCallArray = [cid.toString()] | ||
const callArray = [] | ||
|
||
await walk({ cid, load: loadWrapper(load, callArray) }) | ||
|
||
expectedCallArray.forEach((value, index) => { | ||
same(value, callArray[index]) | ||
}) | ||
}) | ||
}) | ||
}) |