Skip to content

Commit d5949f1

Browse files
authored
feat: upgrade deps and add experimental query resolver (#14)
* feat: upgrade deps and add experimental query resolver * chore: bump version
1 parent 583f00e commit d5949f1

File tree

7 files changed

+276
-14
lines changed

7 files changed

+276
-14
lines changed

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dcdn/graphsync",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"description": "JS implementation of GraphSync and DataTransfer protocols",
55
"main": "dist/src/index.js",
66
"files": [
@@ -27,9 +27,9 @@
2727
},
2828
"type": "module",
2929
"devDependencies": {
30-
"@chainsafe/libp2p-noise": "^9.0.0",
31-
"@libp2p/interface-mocks": "^7.0.3",
32-
"@libp2p/mplex": "^7.0.0",
30+
"@chainsafe/libp2p-noise": "^10.0.1",
31+
"@libp2p/interface-mocks": "^8.0.1",
32+
"@libp2p/mplex": "^7.1.0",
3333
"@types/bl": "^5.0.2",
3434
"@types/mime": "^2.0.3",
3535
"@types/uuid": "^8.3.4",
@@ -43,9 +43,9 @@
4343
"dependencies": {
4444
"@ipld/dag-cbor": "^7.0.1",
4545
"@ipld/dag-pb": "^2.1.16",
46-
"@libp2p/interface-connection": "^3.0.2",
47-
"@libp2p/interface-peer-id": "^1.0.5",
48-
"@libp2p/interface-registrar": "^2.0.3",
46+
"@libp2p/interface-connection": "^3.0.3",
47+
"@libp2p/interface-peer-id": "^1.0.6",
48+
"@libp2p/interface-registrar": "^2.0.4",
4949
"@libp2p/peer-id": "^1.1.16",
5050
"@libp2p/tcp": "^5.0.1",
5151
"@multiformats/multiaddr": "^11.0.5",

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from "./traversal.js";
22
export {GraphSync, Request} from "./graphsync.js";
3-
export {fetch, unixfsPathSelector, getPeer} from "./resolver.js";
3+
export {fetch, unixfsPathSelector, getPeer, resolveQuery} from "./resolver.js";
44
export {push} from "./push.js";

src/resolver.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {
1111
parseContext,
1212
walkBlocks,
1313
SelectorNode,
14+
Selector,
15+
Node,
16+
PathSegment,
17+
ExploreInterpretAs,
1418
} from "./traversal.js";
1519
import mime from "mime/lite.js";
1620
import type {GraphSync} from "./graphsync.js";
@@ -193,3 +197,79 @@ export async function fetch(url: string, init: FetchInit): Promise<Response> {
193197
});
194198
}
195199
}
200+
201+
export async function resolveQuery(
202+
nd: Node,
203+
sel: Selector,
204+
loader: LinkLoader
205+
): Promise<any> {
206+
if (sel instanceof ExploreInterpretAs) {
207+
const reify = loader.reifier(sel.adl);
208+
if (reify) {
209+
const rnd = await reify(nd, loader);
210+
const next = sel.explore(nd, new PathSegment(""));
211+
if (next) {
212+
sel = next;
213+
}
214+
nd = rnd;
215+
}
216+
}
217+
218+
let results: {[key: string]: any} | any[];
219+
220+
switch (nd.kind) {
221+
case Kind.Map:
222+
results = {};
223+
break;
224+
case Kind.List:
225+
results = [];
226+
break;
227+
default:
228+
return nd.value;
229+
}
230+
231+
const attn = sel.interests();
232+
if (attn.length) {
233+
for (const ps of attn) {
234+
let value = await nd.lookupBySegment(ps);
235+
if (value === null) {
236+
break;
237+
}
238+
const sNext = sel.explore(nd.value, ps);
239+
if (sNext !== null) {
240+
if (value.kind === Kind.Link) {
241+
const blk = await loader.load(value.value);
242+
243+
value = new BasicNode(blk.value);
244+
}
245+
246+
const val = await resolveQuery(value, sNext, loader);
247+
if (Array.isArray(results)) {
248+
results.push(val);
249+
} else {
250+
results[ps.value] = val;
251+
}
252+
}
253+
}
254+
} else {
255+
// visit everything
256+
for await (let {pathSegment, value} of nd.entries()) {
257+
const sNext = sel.explore(nd.value, pathSegment);
258+
if (sNext !== null) {
259+
if (value.kind === Kind.Link) {
260+
const blk = await loader.load(value.value);
261+
262+
value = new BasicNode(blk.value);
263+
}
264+
265+
const val = await resolveQuery(value, sNext, loader);
266+
if (Array.isArray(results)) {
267+
results.push(val);
268+
} else {
269+
results[pathSegment.value] = val;
270+
}
271+
}
272+
}
273+
}
274+
return results;
275+
}

src/traversal.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export class BasicNode implements Node {
155155
async lookupBySegment(seg: PathSegment): Promise<Node | null> {
156156
const val = this.value[seg.value];
157157
if (val) {
158-
return val;
158+
return new BasicNode(val);
159159
}
160160
return null;
161161
}
@@ -374,6 +374,8 @@ export function parseContext() {
374374
return this.parseExploreInterpretAs(node[key]);
375375
case ".":
376376
return this.parseMatcher(node[key]);
377+
case "i":
378+
return this.parseExploreIndex(node[key]);
377379
default:
378380
throw new Error("unknown selector");
379381
}
@@ -415,6 +417,21 @@ export function parseContext() {
415417
}
416418
return new ExploreFields(selections, interests);
417419
},
420+
parseExploreIndex(node: any): Selector {
421+
const index = node["i"];
422+
const next = this.parseSelector(node[">"]);
423+
if (typeof index !== "number") {
424+
throw new Error(
425+
"selector spec parse rejected: index field must be present in ExploreIndex selector"
426+
);
427+
}
428+
if (!next) {
429+
throw new Error(
430+
"selector spec parse rejected: next field must be present in ExploreIndex selector"
431+
);
432+
}
433+
return new ExploreIndex(next, index);
434+
},
418435
parseExploreInterpretAs(node: any): Selector {
419436
const adl = node["as"];
420437
const next: SelectorNode = node[">"];
@@ -499,6 +516,30 @@ export class ExploreFields implements Selector {
499516
}
500517
}
501518

519+
export class ExploreIndex implements Selector {
520+
next: Selector;
521+
interest: [PathSegment];
522+
constructor(next: Selector, index: number) {
523+
this.next = next;
524+
this.interest = [new PathSegment(index)];
525+
}
526+
interests(): PathSegment[] {
527+
return this.interest;
528+
}
529+
explore(node: any, path: PathSegment): Selector | null {
530+
if (!Array.isArray(node)) {
531+
return null;
532+
}
533+
if (path.toIndex() === this.interest[0].toIndex()) {
534+
return this.next;
535+
}
536+
return null;
537+
}
538+
decide(node: any): boolean {
539+
return false;
540+
}
541+
}
542+
502543
export class ExploreInterpretAs implements Selector {
503544
next: Selector;
504545
adl: string;
@@ -656,9 +697,9 @@ export async function unixfsReifier(
656697
try {
657698
const unixfs = UnixFS.unmarshal(node.value.Data);
658699
if (unixfs.isDirectory()) {
659-
const dir: {[key: string]: Node} = {};
700+
const dir: {[key: string]: CID} = {};
660701
for (const link of node.value.Links) {
661-
dir[link.Name] = new BasicNode(link.Hash);
702+
dir[link.Name] = link.Hash;
662703
}
663704
return new BasicNode(dir);
664705
}

test/listening.node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {expect} from "aegir/chai";
22
import {createLibp2p, Libp2p} from "libp2p";
33
import {mplex} from "@libp2p/mplex";
44
import {tcp} from "@libp2p/tcp";
5-
import {Noise} from "@chainsafe/libp2p-noise";
5+
import {noise} from "@chainsafe/libp2p-noise";
66
import {MemoryBlockstore} from "blockstore-core/memory";
77
import {importer} from "ipfs-unixfs-importer";
88
import {GraphSync} from "../src/graphsync.js";
@@ -16,7 +16,7 @@ async function createNode(): Promise<Libp2p> {
1616
},
1717
streamMuxers: [mplex()],
1818
transports: [tcp()],
19-
connectionEncryption: [() => new Noise()],
19+
connectionEncryption: [noise()],
2020
});
2121
await node.start();
2222
return node;

test/resolver.spec.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import {expect} from "aegir/chai";
2-
import {resolve, unixfsPathSelector, getPeer} from "../src/resolver.js";
2+
import {
3+
resolve,
4+
unixfsPathSelector,
5+
getPeer,
6+
resolveQuery,
7+
} from "../src/resolver.js";
8+
import {
9+
selectorBuilder as sb,
10+
BasicNode,
11+
parseContext,
12+
LinkSystem,
13+
} from "../src/traversal.js";
314
import {MemoryBlockstore} from "blockstore-core/memory";
415
import {MockLibp2p, concatChunkIterator} from "./mock-libp2p.js";
516
import {peerIdFromString} from "@libp2p/peer-id";
617
import {GraphSync} from "../src/graphsync.js";
718
import {importer} from "ipfs-unixfs-importer";
19+
import {encode} from "multiformats/block";
20+
import * as codec from "@ipld/dag-cbor";
21+
import {sha256 as hasher} from "multiformats/hashes/sha2";
822

923
describe("resolver", () => {
1024
it("parse a unixfs path", () => {
@@ -68,4 +82,74 @@ describe("resolver", () => {
6882
);
6983
expect(multiaddrs[0].protos()[0].name).to.equal("ip4");
7084
});
85+
86+
it("resolves a query", async () => {
87+
const blocks = new MemoryBlockstore();
88+
89+
const account1 = {
90+
balance: 300,
91+
lastUpdated: "yesterday",
92+
};
93+
const account1Block = await encode({value: account1, codec, hasher});
94+
await blocks.put(account1Block.cid, account1Block.bytes);
95+
96+
const account2 = {
97+
balance: 100,
98+
lastUpdated: "now",
99+
};
100+
const account2Block = await encode({value: account2, codec, hasher});
101+
await blocks.put(account2Block.cid, account2Block.bytes);
102+
103+
const state = {
104+
"0x01": account1Block.cid,
105+
"0x02": account2Block.cid,
106+
};
107+
const stateBlock = await encode({value: state, codec, hasher});
108+
await blocks.put(stateBlock.cid, stateBlock.bytes);
109+
110+
const msg = {
111+
from: "0x01",
112+
to: "0x02",
113+
amount: 100,
114+
};
115+
const msgBlock = await encode({value: msg, codec, hasher});
116+
await blocks.put(msgBlock.cid, msgBlock.bytes);
117+
118+
const root = {
119+
state: stateBlock.cid,
120+
epoch: Date.now(),
121+
messages: [msgBlock.cid],
122+
};
123+
const rootBlock = await encode({value: root, codec, hasher});
124+
await blocks.put(rootBlock.cid, rootBlock.bytes);
125+
126+
const selector = sb.exploreFields({
127+
state: sb.exploreFields({
128+
"0x02": sb.exploreFields({
129+
balance: sb.match(),
130+
lastUpdated: sb.match(),
131+
}),
132+
}),
133+
messages: sb.exploreIndex(
134+
0,
135+
sb.exploreFields({
136+
amount: sb.match(),
137+
})
138+
),
139+
});
140+
const sel = parseContext().parseSelector(selector);
141+
const ls = new LinkSystem(blocks);
142+
143+
const result = await resolveQuery(new BasicNode(root), sel, ls);
144+
145+
expect(result).to.deep.equal({
146+
state: {
147+
"0x02": {
148+
balance: 100,
149+
lastUpdated: "now",
150+
},
151+
},
152+
messages: [{amount: 100}],
153+
});
154+
});
71155
});

test/traversal.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,61 @@ describe("traversal", () => {
225225
);
226226
expect(selector3).to.deep.equal(unixfsSelector);
227227
});
228+
229+
it("selects an index", async () => {
230+
const bs = new MemoryBlockstore();
231+
232+
const first = new Uint8Array(5 * 256);
233+
const second = new Uint8Array(3 * 256);
234+
const third = new Uint8Array(2 * 256);
235+
const forth = new Uint8Array(4 * 256);
236+
237+
// chunk and dagify it then get the root cid
238+
let cid;
239+
for await (const chunk of importer(
240+
[
241+
{path: "first", content: first},
242+
{path: "second", content: second},
243+
{path: "/children/third", content: third},
244+
{path: "/children/forth", content: forth},
245+
],
246+
bs,
247+
{
248+
cidVersion: 1,
249+
maxChunkSize: 256,
250+
rawLeaves: true,
251+
wrapWithDirectory: true,
252+
}
253+
)) {
254+
if (chunk.path === "") {
255+
cid = chunk.cid;
256+
}
257+
}
258+
const selector = builder.exploreFields({
259+
Links: builder.exploreIndex(
260+
0,
261+
builder.exploreRecursive(
262+
builder.exploreAll(builder.edge()),
263+
builder.depth(2)
264+
)
265+
),
266+
});
267+
268+
const sel = parseContext().parseSelector(selector);
269+
270+
const source = new LinkSystem(bs);
271+
272+
let last;
273+
for await (const blk of walkBlocks(new BasicNode(cid), sel, source)) {
274+
last = blk;
275+
}
276+
277+
if (!last) {
278+
throw new Error("failed traversal");
279+
}
280+
281+
expect(last.cid.toString()).to.equal(
282+
"bafybeiepvdqmdakhtwotvykxujrmt5fcq4xca5jmoo6wzxhjk3q3pqe4te"
283+
);
284+
});
228285
});

0 commit comments

Comments
 (0)