Skip to content

Commit

Permalink
feat: lazy-loaded http client
Browse files Browse the repository at this point in the history
- remove external dependency
- support window.IpfsHttpClient
- remove defaultApiAddress (simplifies api)

License: MIT
Signed-off-by: Marcin Rataj <[email protected]>
  • Loading branch information
lidel committed Jan 6, 2020
1 parent 5081622 commit 99807d9
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 50 deletions.
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Build Status](https://flat.badgen.net/travis/ipfs-shipyard/ipfs-provider)](https://travis-ci.com/ipfs-shipyard/ipfs-provider)
[![Dependency Status](https://david-dm.org/ipfs-shipyard/ipfs-provider.svg?style=flat-square)](https://david-dm.org/ipfs-shipyard/ipfs-provider)

> This module tries to connect to IPFS via multiple [providers](#providers).
> Returns IPFS API by trying multiple [providers](#providers) in a custom fallback order.
> It is a general-purpose replacement for [ipfs-redux-bundle](https://github.com/ipfs-shipyard/ipfs-redux-bundle).
- [Install](#install)
Expand Down Expand Up @@ -85,21 +85,24 @@ Please keep in mind that all of these have defaults and you **do not** need to s

### `httpClient`

Tries to connect to HTTP API via [`js-ipfs-http-client`](https://github.com/ipfs/js-ipfs-http-client) with either a user provided `apiAddress`, the current origin, or `defaultApiAddress`.
Tries to connect to HTTP API via [`js-ipfs-http-client`](https://github.com/ipfs/js-ipfs-http-client).
This provider will establish connection with `apiAddress`, the current origin, or the default local API address (`/ip4/127.0.0.1/tcp/5001`).

Value provided in `apiAddress` can be:
The client library is initialized using constructor returned by `getConstructor` function or the one exposed at `window.IpfsHttpClient`.
Supports lazy-loading and small bundle sizes.

Value provided in `apiAddress` can be:
- a multiaddr (string like `/ip4/127.0.0.1/tcp/5001` or an [object](https://github.com/multiformats/js-multiaddr/))
- a String with an URL (`https://example.com:8080/`)
- a String with an URL (`https://api.example.com:8080/`)
- a configuration object supported by [`js-ipfs-http-client`](https://github.com/ipfs/js-ipfs-http-client#importing-the-module-and-usage)

(`{ host: '1.1.1.1', port: '80', apiPath: '/ipfs/api/v0' }`)

```js
const { ipfs, provider } = await getIpfs({
providers: [
httpClient({
// defaults
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
apiAddress: null
getConstructor: () => import('ipfs-http-client'),
apiAddress: 'https://api.example.com:8080/'
})
]
})
Expand All @@ -108,7 +111,6 @@ const { ipfs, provider } = await getIpfs({
To try multiple endpoints, simply use this provider multiple times.
See [`examples/browser-browserify/src/index.js`](./examples/browser-browserify/src/index.js) for real world example.


### `jsIpfs`

Spawns embedded [`js-ipfs`](https://github.com/ipfs/js-ipfs) (full node in JavaScript)
Expand All @@ -118,7 +120,6 @@ in the context of the current page using customizable constructor:
const { ipfs, provider } = await getIpfs({
providers: [
jsIpfs({
// defaults
getConstructor: () => import('ipfs'),
options: { /* advanced config */ }
})
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@
},
"homepage": "https://github.com/ipfs-shipyard/ipfs-provider#readme",
"dependencies": {
"ipfs-http-client": "^40.1.0",
"merge-options": "^2.0.0",
"window-or-global": "^1.0.1"
"merge-options": "^2.0.0"
},
"devDependencies": {
"ipfs-http-client": "^40.1.0",
"jest": "^24.9.0",
"standard": "^14.3.1"
}
Expand Down
5 changes: 5 additions & 0 deletions src/constants/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict'

module.exports = {
DEFAULT_HTTP_API: '/ip4/127.0.0.1/tcp/5001'
}
8 changes: 8 additions & 0 deletions src/constants/root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict'
/* global self */

// Establish the root object, `window` in the browser, `self` in Service Worker. or `global` on the server.
// Credit: https://github.com/megawac/underscore/commit/365311c9a440438531ca1c6bfd49e3c7c5f46079
module.exports = (typeof self === 'object' && self.self === self && self) ||
(typeof global === 'object' && global.global === global && global) ||
this
10 changes: 2 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
'use strict'

const root = require('window-or-global')
const httpClient = require('ipfs-http-client')
const root = require('./constants/root')
const mergeOptions = require('merge-options')

const tryWebExt = require('./providers/webext')
const tryWindow = require('./providers/window-ipfs')
const tryHttpClient = require('./providers/http-client')
Expand All @@ -27,11 +25,7 @@ const makeProvider = (fn, defaults = {}) => {

const providers = {
httpClient: makeProvider((options) => {
const { location } = root
return tryHttpClient({ httpClient, location, ...options })
}, {
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
apiAddress: null
return tryHttpClient({ root, ...options })
}),
windowIpfs: makeProvider(options => {
return tryWindow({ root, ...options })
Expand Down
42 changes: 36 additions & 6 deletions src/providers/http-client.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
'use strict'

const PROVIDERS = require('../constants/providers')
const { DEFAULT_HTTP_API } = require('../constants/defaults')

/*
* This provider lazy-loads https://github.com/ipfs/js-ipfs-http-client
* so it is not included as a dependency if not used.
*
* HTTP Client init fallback:
* 1. Use constructor returned by getConstructor function
* 2. Fallback to window.IpfsHttpClient
*
* API URL fallback order:
* 1. Try user specified API address
* 2. Try current origin
* 3. Try DEFAULT_HTTP_API
*/
async function tryHttpClient ({ getConstructor, apiAddress, root, connectionTest }) {
// Find HTTP client
let httpClient
if (getConstructor) httpClient = await getConstructor()

// Final fllback to window.IpfsHttpClient or error
if (!httpClient) {
if (root.IpfsHttpClient) {
httpClient = root.IpfsHttpClient
} else {
throw new Error('ipfs-provider could not initialize js-ipfs-http-client: make sure its constructor is returned by getConstructor function or exposed at window.IpfsHttpClient')
}
}

// Allow the use of `import` or `require` on `getConstructor` fn
httpClient = httpClient.default || httpClient // TODO: create 'import' demo in examples/

// 1. Try user specified API address
// 2. Try current origin
// 3. Try multiaddr from defaultApiAddress
async function tryHttpClient ({ httpClient, apiAddress, defaultApiAddress, location, connectionTest }) {
// Explicit custom apiAddress provided. Only try that.
if (apiAddress) {
return maybeApi({ apiAddress, connectionTest, httpClient })
}

// Current origin is not localhost:5001 so try with current origin info
const { location } = root
if (location && !(location.port === '5001' && location.hostname.match(/^127.0.0.1$|^localhost$/))) {
const origin = new URL(location.origin)
origin.pathname = '/'
Expand All @@ -24,17 +52,19 @@ async function tryHttpClient ({ httpClient, apiAddress, defaultApiAddress, locat
}

// ...otherwise try /ip4/127.0.0.1/tcp/5001
return maybeApi({ apiAddress: defaultApiAddress, connectionTest, httpClient })
return maybeApi({ apiAddress: DEFAULT_HTTP_API, connectionTest, httpClient })
}

// Helper to construct and test an api client. Returns an js-ipfs-api instance or null
// Init and test an api client against provded API address.
// Returns js-ipfs-http-client instance or null
async function maybeApi ({ apiAddress, connectionTest, httpClient }) {
try {
const ipfs = httpClient(apiAddress)
await connectionTest(ipfs)
return { ipfs, provider: PROVIDERS.httpClient, apiAddress }
} catch (error) {
// Failed to connect to ipfs-api in `apiAddress`
return null
}
}

Expand Down
105 changes: 82 additions & 23 deletions test/providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,14 @@ describe('provider: window.ipfs', () => {
})
})

describe('provider: ipfs-http-api', () => {
describe('provider: httpClient', () => {
it('should use the apiAddress (implicit http)', async () => {
const opts = {
apiAddress: '/ip4/1.1.1.1/tcp/1111',
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
location: new URL('http://localhost:5001'),
httpClient,
root: {
location: new URL('http://localhost:5001')
},
getConstructor: () => httpClient,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { ipfs, provider, apiAddress } = await tryHttpClient(opts)
Expand All @@ -107,9 +108,10 @@ describe('provider: ipfs-http-api', () => {
it('should use the apiAddress (explicit https)', async () => {
const opts = {
apiAddress: '/ip4/1.1.1.1/tcp/1111/https',
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
location: new URL('http://localhost:5001'),
httpClient,
root: {
location: new URL('http://localhost:5001')
},
getConstructor: () => httpClient,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { ipfs, provider, apiAddress } = await tryHttpClient(opts)
Expand All @@ -124,9 +126,10 @@ describe('provider: ipfs-http-api', () => {

it('should use the implicit http:// location where origin is on http', async () => {
const opts = {
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
location: new URL('http://dev.local:5001/subdir/some-page.html'),
httpClient,
root: {
location: new URL('http://dev.local:5001/subdir/some-page.html')
},
getConstructor: () => httpClient,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { ipfs, provider, apiAddress } = await tryHttpClient(opts)
Expand All @@ -141,9 +144,10 @@ describe('provider: ipfs-http-api', () => {

it('should use the implicit https:// location where origin is on https', async () => {
const opts = {
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
location: new URL('https://dev.local:5001/subdir/some-page.html'),
httpClient,
root: {
location: new URL('https://dev.local:5001/subdir/some-page.html')
},
getConstructor: () => httpClient,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { ipfs, provider, apiAddress } = await tryHttpClient(opts)
Expand All @@ -156,39 +160,94 @@ describe('provider: ipfs-http-api', () => {
expect(config.protocol).toEqual('https')
})

it('should use the location where port not 5001', async () => {
it('should try API at window.location.origin', async () => {
const fakeHttpClient = jest.fn()
const opts = {
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
location: new URL('http://localhost:9999/subdir/some-page.html'),
httpClient: jest.fn(),
root: {
location: new URL('http://localhost:9999/subdir/some-page.html')
},
getConstructor: () => fakeHttpClient,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { provider, apiAddress } = await tryHttpClient(opts)
expect(apiAddress).toEqual('http://localhost:9999/')
expect(provider).toEqual(PROVIDERS.httpClient)
expect(opts.connectionTest.mock.calls.length).toBe(1)
expect(opts.httpClient.mock.calls.length).toBe(1)
expect(fakeHttpClient.mock.calls.length).toBe(1)
})

it('should use the defaultApiAddress if location fails', async () => {
it('should use the DEFAULT_HTTP_API if location fails', async () => {
const opts = {
defaultApiAddress: '/ip4/127.0.0.1/tcp/5001',
location: new URL('http://astro.cat:5001'),
httpClient,
root: {
location: new URL('http://astro.cat:5001')
},
getConstructor: () => httpClient,
// location call fails, default ok
connectionTest: jest.fn()
.mockRejectedValueOnce(new Error('nope'))
.mockResolvedValueOnce(true)
}
const { ipfs, provider, apiAddress } = await tryHttpClient(opts)
expect(apiAddress).toEqual(opts.defaultApiAddress)
expect(apiAddress).toEqual('/ip4/127.0.0.1/tcp/5001')
expect(provider).toEqual(PROVIDERS.httpClient)
expect(opts.connectionTest.mock.calls.length).toBe(2)
const config = ipfs.getEndpointConfig()
expect(config.host).toEqual('127.0.0.1')
expect(config.port).toEqual('5001')
expect(config.protocol).toEqual('http')
})

it('should use window.IpfsHttpClient if present and no getConstructor is provided', async () => {
const opts = {
apiAddress: '/ip4/1.2.3.4/tcp/1111/https',
root: {
IpfsHttpClient: httpClient,
location: new URL('http://example.com')
},
getConstructor: undefined, // (missing on purpose)
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { ipfs, provider, apiAddress } = await tryHttpClient(opts)
expect(apiAddress).toEqual(opts.apiAddress)
expect(provider).toEqual(PROVIDERS.httpClient)
expect(opts.connectionTest.mock.calls.length).toBe(1)
const config = ipfs.getEndpointConfig()
expect(config.host).toEqual('1.2.3.4')
expect(config.port).toEqual('1111')
expect(config.protocol).toEqual('https')
})

it('should prefer getConstructor over window.IpfsHttpClient', async () => {
const constructorHttpClient = jest.fn()
const windowHttpClient = jest.fn()
const opts = {
apiAddress: '/ip4/1.2.3.4/tcp/1111/https',
root: {
IpfsHttpClient: windowHttpClient,
location: new URL('http://example.com')
},
getConstructor: () => constructorHttpClient,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const { apiAddress } = await tryHttpClient(opts)
expect(apiAddress).toEqual(opts.apiAddress)
expect(windowHttpClient.mock.calls.length).toBe(0)
expect(constructorHttpClient.mock.calls.length).toBe(1)
})

it('should throw is no getConstructor nor window.IpfsHttpClient is provided', async () => {
const opts = {
apiAddress: '/ip4/1.2.3.4/tcp/1111/https',
root: {
IpfsHttpClient: undefined,
location: new URL('http://example.com')
},
getConstructor: undefined,
connectionTest: jest.fn().mockResolvedValueOnce(true)
}
const expectedError = new Error('ipfs-provider could not initialize js-ipfs-http-client: make sure its constructor is returned by getConstructor function or exposed at window.IpfsHttpClient')
expect(tryHttpClient(opts)).rejects.toEqual(expectedError)
})
})

describe('provider: js-ipfs', () => {
Expand Down

0 comments on commit 99807d9

Please sign in to comment.