diff --git a/README.md b/README.md index a8ccd7a..9efde84 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,49 @@ -# @libp2p/http-v1-content-routing +# @libp2p/delegated-routing-v1-http-api-content-routing [![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) -[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-http-v1-content-routing.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-http-v1-content-routing) -[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-http-v1-content-routing/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/libp2p/js-http-v1-content-routing/actions/workflows/js-test-and-release.yml?query=branch%3Amain) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-delegated-routing-v1-http-api-content-routing.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-delegated-routing-v1-http-api-content-routing) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-delegated-routing-v1-http-api-content-routing/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/libp2p/js-delegated-routing-v1-http-api-content-routing/actions/workflows/js-test-and-release.yml?query=branch%3Amain) -> Use a Routing V1 HTTP service to discover content providers +> Use a Delegated Routing V1 HTTP service to discover content providers + +This is a [ContentRouting](https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface.content_routing.ContentRouting.html) +implementation that makes use of the [@helia/delegated-routing-v1-http-api-client](https://www.npmjs.com/package/@helia/delegated-routing-v1-http-api-client) +to use servers that implement the snappily-titled [Delegated Routing V1 HTTP API](Delegated Routing V1 HTTP API) +spec to get/put IPNS records and to resolve providers for CIDs. ## Table of contents -- [Install](#install) - - [Browser ` + ``` -## Example +# Example ```js import { createLibp2p } from 'libp2p' -import { reframeContentRouting } from '@libp2p/reframe-content-routing' +import { delgatedRoutingV1HTTPAPIContentRouting } from '@libp2p/delegated-routing-http-v1-content-routing' const node = await createLibp2p({ contentRouters: [ - reframeContentRouting('https://cid.contact/reframe') + delgatedRoutingV1HTTPAPIContentRouting('https://example.org') ] //.. other config }) @@ -51,7 +56,7 @@ for await (const provider of node.contentRouting.findProviders('cid')) { ## API Docs -- +- ## License diff --git a/package.json b/package.json index 6796171..7a583c6 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { - "name": "@libp2p/http-v1-content-routing", - "version": "1.0.2", - "description": "Use a Routing V1 HTTP service to discover content providers", + "name": "@libp2p/delegated-routing-v1-http-api-content-routing", + "version": "0.0.0", + "description": "Use a Delegated Routing V1 HTTP service to discover content providers", "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/libp2p/js-http-v1-content-routing#readme", + "homepage": "https://github.com/libp2p/js-delegated-routing-v1-http-api-content-routing#readme", "repository": { "type": "git", - "url": "git+https://github.com/libp2p/js-http-v1-content-routing.git" + "url": "git+https://github.com/libp2p/js-delegated-routing-v1-http-api-content-routing.git" }, "bugs": { - "url": "https://github.com/libp2p/js-http-v1-content-routing/issues" + "url": "https://github.com/libp2p/js-delegated-routing-v1-http-api-content-routing/issues" }, "type": "module", "types": "./dist/src/index.d.ts", @@ -130,20 +130,17 @@ "test:node": "aegir test -t node --cov", "test:electron-main": "aegir test -t electron-main", "release": "aegir release", - "docs": "aegir docs" + "docs": "aegir docs -- --includeVersion false" }, "dependencies": { + "@helia/delegated-routing-v1-http-api-client": "^1.0.1", "@libp2p/interface": "^0.1.1", "@libp2p/logger": "^3.0.1", "@libp2p/peer-id": "^3.0.1", - "@multiformats/multiaddr": "^12.1.2", - "any-signal": "^4.1.1", - "browser-readablestream-to-it": "^2.0.2", - "it-to-buffer": "^4.0.1", + "ipns": "^7.0.1", + "it-map": "^3.0.4", "multiformats": "^12.0.1", - "p-defer": "^4.0.0", - "p-queue": "^7.3.4", - "uint8arrays": "^4.0.3" + "uint8arrays": "^4.0.6" }, "devDependencies": { "@libp2p/peer-id-factory": "^3.0.2", diff --git a/src/index.ts b/src/index.ts index 1b0fa89..38c5517 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,31 @@ +import { type DelegatedRoutingV1HttpApiClient, createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client' import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' -import { peerIdFromString } from '@libp2p/peer-id' -import { multiaddr } from '@multiformats/multiaddr' -import { anySignal } from 'any-signal' -import toIt from 'browser-readablestream-to-it' -import toBuffer from 'it-to-buffer' -import defer from 'p-defer' -import PQueue from 'p-queue' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { peerIdFromBytes } from '@libp2p/peer-id' +import { marshal, unmarshal } from 'ipns' +import map from 'it-map' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import type { AbortOptions } from '@libp2p/interface' import type { ContentRouting } from '@libp2p/interface/content-routing' +import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { Startable } from '@libp2p/interface/startable' -import type { Multiaddr } from '@multiformats/multiaddr' import type { CID } from 'multiformats/cid' -const log = logger('reframe-content-routing') +const log = logger('delegated-routing-v1-http-api-content-routing') -export interface ReframeV1Response { - Providers: ReframeV1ResponseItem[] +const IPNS_PREFIX = uint8ArrayFromString('/ipns/') + +function isIPNSKey (key: Uint8Array): boolean { + return uint8ArrayEquals(key.subarray(0, IPNS_PREFIX.byteLength), IPNS_PREFIX) } -export interface ReframeV1ResponseItem { - ID: string - Addrs: string[] - Protocol: string - Schema: string +const peerIdFromRoutingKey = (key: Uint8Array): PeerId => { + return peerIdFromBytes(key.slice(IPNS_PREFIX.length)) } -export interface ReframeContentRoutingInit { +export interface DelegatedRoutingV1HTTPAPIContentRoutingInit { /** * A concurrency limit to avoid request flood in web browser (default: 4) * @@ -42,33 +39,21 @@ export interface ReframeContentRoutingInit { timeout?: number } -const defaultValues = { - concurrentRequests: 4, - timeout: 30e3 -} - /** * An implementation of content routing, using a delegated peer */ -class ReframeContentRouting implements ContentRouting, Startable { +class DelegatedRoutingV1HTTPAPIContentRouting implements ContentRouting, Startable { private started: boolean - private readonly httpQueue: PQueue - private readonly shutDownController: AbortController - private readonly clientUrl: URL - private readonly timeout: number + private readonly client: DelegatedRoutingV1HttpApiClient /** * Create a new DelegatedContentRouting instance */ - constructor (url: string | URL, init: ReframeContentRoutingInit = {}) { + constructor (url: string | URL, init: DelegatedRoutingV1HTTPAPIContentRoutingInit = {}) { this.started = false - this.shutDownController = new AbortController() - this.httpQueue = new PQueue({ - concurrency: init.concurrentRequests ?? defaultValues.concurrentRequests - }) - this.clientUrl = url instanceof URL ? url : new URL(url) - this.timeout = init.timeout ?? defaultValues.timeout - log('enabled Reframe routing via', url) + this.client = createDelegatedRoutingV1HttpApiClient(new URL(url), init) + + log('enabled Delegated Routing V1 HTTP API Content Routing via', url) } isStarted (): boolean { @@ -80,85 +65,47 @@ class ReframeContentRouting implements ContentRouting, Startable { } stop (): void { - this.httpQueue.clear() - this.shutDownController.abort() + this.client.stop() this.started = false } - async * findProviders (key: CID, options: AbortOptions = {}): AsyncIterable { - log('findProviders starts: %c', key) - - const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)]) - const onStart = defer() - const onFinish = defer() - - void this.httpQueue.add(async () => { - onStart.resolve() - return onFinish.promise - }) - - try { - await onStart.promise - - // https://github.com/ipfs/specs/blob/main/routing/ROUTING_V1_HTTP.md#api - const resource = `${this.clientUrl}routing/v1/providers/${key.toString()}` - const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal } - const a = await fetch(resource, getOptions) - - if (a.body == null) { - throw new CodeError('Reframe response had no body', 'ERR_BAD_RESPONSE') + async * findProviders (cid: CID, options: AbortOptions = {}): AsyncIterable { + yield * map(this.client.getProviders(cid, options), (record) => { + return { + id: record.ID, + multiaddrs: record.Addrs ?? [], + protocols: [] } - - const body = await toBuffer(toIt(a.body)) - const result: ReframeV1Response = JSON.parse(uint8ArrayToString(body)) - - for await (const event of result.Providers) { - if (event.Protocol !== 'transport-bitswap' || event.Schema !== 'bitswap') { - continue - } - - yield this.mapEvent(event) - } - } catch (err) { - log.error('findProviders errored:', err) - } finally { - signal.clear() - onFinish.resolve() - log('findProviders finished: %c', key) - } + }) } - private mapEvent (event: ReframeV1ResponseItem): PeerInfo { - const peer = peerIdFromString(event.ID) - const ma: Multiaddr[] = [] + async provide (): Promise { + // noop + } - for (const strAddr of event.Addrs) { - const addr = multiaddr(strAddr) - ma.push(addr) + async put (key: Uint8Array, value: Uint8Array, options?: AbortOptions): Promise { + if (!isIPNSKey(key)) { + return } - const pi = { - id: peer, - multiaddrs: ma, - protocols: [] - } + const peerId = peerIdFromRoutingKey(key) + const record = unmarshal(value) - return pi + await this.client.putIPNS(peerId, record, options) } - async provide (): Promise { - // noop - } + async get (key: Uint8Array, options?: AbortOptions): Promise { + if (!isIPNSKey(key)) { + throw new CodeError('Not found', 'ERR_NOT_FOUND') + } - async put (): Promise { - // noop - } + const peerId = peerIdFromRoutingKey(key) + const record = await this.client.getIPNS(peerId, options) - async get (): Promise { - throw new CodeError('Not found', 'ERR_NOT_FOUND') + return marshal(record) } } -export function reframeContentRouting (url: string | URL, init: ReframeContentRoutingInit = {}): () => ContentRouting { - return () => new ReframeContentRouting(url, init) +export function delgatedRoutingV1HTTPAPIContentRouting (url: string | URL, init: DelegatedRoutingV1HTTPAPIContentRoutingInit = {}): () => ContentRouting { + return () => new DelegatedRoutingV1HTTPAPIContentRouting(url, init) } diff --git a/test/index.spec.ts b/test/index.spec.ts index 8a6237e..59ee6b7 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -4,7 +4,7 @@ import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' import all from 'it-all' import { CID } from 'multiformats/cid' -import { type ReframeV1ResponseItem, reframeContentRouting } from '../src/index.js' +import { delgatedRoutingV1HTTPAPIContentRouting } from '../src/index.js' if (process.env.ECHO_SERVER == null) { throw new Error('Echo server not configured correctly') @@ -15,9 +15,9 @@ const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') describe('ReframeContentRouting', function () { it('should find providers', async () => { - const providers: ReframeV1ResponseItem[] = [{ - Protocol: 'transport-bitswap', - Schema: 'bitswap', + const providers = [{ + Protocols: ['transport-bitswap'], + Schema: 'peer', ID: (await createEd25519PeerId()).toString(), Addrs: ['/ip4/41.41.41.41/tcp/1234'] }, { @@ -25,17 +25,19 @@ describe('ReframeContentRouting', function () { Schema: 'bitswap', ID: (await createEd25519PeerId()).toString(), Addrs: ['/ip4/42.42.42.42/tcp/1234'] + }, { + Schema: 'unknown', + ID: (await createEd25519PeerId()).toString(), + Addrs: ['/ip4/42.42.42.42/tcp/1234'] }] // load providers for the router to fetch await fetch(`${process.env.ECHO_SERVER}/add-providers/${cid.toString()}`, { method: 'POST', - body: JSON.stringify({ - Providers: providers - }) + body: providers.map(prov => JSON.stringify(prov)).join('\n') }) - const routing = reframeContentRouting(serverUrl)() + const routing = delgatedRoutingV1HTTPAPIContentRouting(serverUrl)() const provs = await all(routing.findProviders(cid)) expect(provs.map(prov => ({ @@ -54,7 +56,7 @@ describe('ReframeContentRouting', function () { body: 'not json' }) - const routing = reframeContentRouting(serverUrl)() + const routing = delgatedRoutingV1HTTPAPIContentRouting(serverUrl)() const provs = await all(routing.findProviders(cid)) expect(provs).to.be.empty() @@ -84,14 +86,14 @@ describe('ReframeContentRouting', function () { }) }) - const routing = reframeContentRouting(serverUrl)() + const routing = delgatedRoutingV1HTTPAPIContentRouting(serverUrl)() const provs = await all(routing.findProviders(cid)) expect(provs).to.be.empty() }) it('should handle empty input', async () => { - const routing = reframeContentRouting(serverUrl)() + const routing = delgatedRoutingV1HTTPAPIContentRouting(serverUrl)() const provs = await all(routing.findProviders(cid)) expect(provs).to.be.empty()