diff --git a/.aegir.js b/.aegir.js new file mode 100644 index 0000000..05b296b --- /dev/null +++ b/.aegir.js @@ -0,0 +1,25 @@ +export default { + tsRepo: false, + build: { + config: { + format: 'esm', + banner: { + js: '' + }, + footer: { + js: '' + } + } + }, + test: { + before: (...args) => { + if (args[0].runner === 'node') { + return { + env: { + NODE_OPTIONS: '--loader=esmock' + } + } + } + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8ac209c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root=true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d28495b..f4be23a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,8 +44,8 @@ jobs: - uses: actions/checkout@v2 - uses: microsoft/playwright-github-action@v1 - run: npm install - - run: npx aegir test -t browser --bail --cov - - run: npx aegir test -t webworker --bail + - run: npx aegir test -t browser --bail --cov --files test/**/*.browser.spec.{js,cjs,mjs} + - run: npx aegir test -t webworker --bail --files test/**/*.browser.spec.{js,cjs,mjs} - uses: codecov/codecov-action@v1 test-firefox: needs: check @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@v2 - uses: microsoft/playwright-github-action@v1 - run: npm install - - run: npx aegir test -t browser -t webworker --bail -- --browser firefox + - run: npx aegir test -t browser -t webworker --bail --files test/**/*.browser.spec.{js,cjs,mjs} -- --browser firefox test-webkit: needs: check runs-on: ubuntu-latest @@ -62,11 +62,11 @@ jobs: - uses: actions/checkout@v2 - uses: microsoft/playwright-github-action@v1 - run: npm install - - run: npx aegir test -t browser -t webworker --bail -- --browser webkit + - run: npx aegir test -t browser -t webworker --bail --files test/**/*.browser.spec.{js,cjs,mjs} -- --browser webkit test-electron-main: needs: check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: npm install - - run: npx xvfb-maybe aegir test -t electron-main --bail + - run: npx xvfb-maybe aegir test -t electron-main --bail --files test/**/*.browser.spec.{js,cjs,mjs} diff --git a/README.md b/README.md index 0879e38..e8fbc8d 100644 --- a/README.md +++ b/README.md @@ -42,39 +42,54 @@ npm install --save ipfs-geoip ### CDN -Instead of a local installation (and browserification) you may request a [remote copy from jsDelivr](https://www.jsdelivr.com/package/npm/ipfs-geoip): +Instead of a local installation (and browserification) you may request a specific +version `N.N.N` as a [remote copy from jsDelivr](https://www.jsdelivr.com/package/npm/ipfs-geoip): ```html - - + ``` -When using prebuilt bundle from CDN, `ipfs-geoip` will be exposed under `window.IpfsGeoip` - +The response in the console should look similar to: +```js +{ + "country_name": "USA", + "country_code": "US", + "region_code": "VA", + "city": "Ashburn", + "postal_code": "20149", + "latitude": 39.0469, + "longitude": -77.4903, + "planet": "Earth" +} +``` ## Usage ### With public gateways (default) -If `ipfs` is a string or array of strings with public gateway URLs, it will be used for +If `gateways` is a string or array of strings with public gateway URLs, it will be used for fetching IPFS blocks as [`application/vnd.ipld.raw`](https://www.iana.org/assignments/media-types/application/vnd.ipld.raw) -and parsing them as DAG-CBOR locally: +and parsing them as DAG-CBOR locally via [@ipld/dag-cbor](https://www.npmjs.com/package/@ipld/dag-cbor): ```js const geoip = require('ipfs-geoip') const exampleIp = '66.6.44.4' -const ipfsGw = ['https://ipfs.io', 'https://dweb.link'] +const gateways = ['https://ipfs.io', 'https://dweb.link'] try { - const result = await geoip.lookup(ipfsGw, exampleIp) + const result = await geoip.lookup(gateways, exampleIp) console.log('Result: ', result) } catch (err) { console.log('Error: ' + err) } try { - const result = await geoip.lookupPretty(ipfsGw, '/ip4/' + exampleIp) + const result = await geoip.lookupPretty(gateways, '/ip4/' + exampleIp) console.log('Pretty result: %s', result.formatted) } catch (err) { console.log('Error: ' + err) diff --git a/example/lookup.js b/example/lookup.js index b288ee8..db7bbc4 100644 --- a/example/lookup.js +++ b/example/lookup.js @@ -1,5 +1,4 @@ import * as geoip from '../src/index.js' -import { create } from 'ipfs-http-client' const ipfsGw = process?.env?.IPFS_GATEWAY || 'https://ipfs.io' diff --git a/package-lock.json b/package-lock.json index a6f2865..095da1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "csv-parse": "^5.3.0", + "esmock": "^2.0.6", "gauge": "^4.0.4", "ipfs-http-client": "^58.0.1", "multihashes": "^4.0.3", @@ -8033,6 +8034,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esmock": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.0.6.tgz", + "integrity": "sha512-2EY6isqPxZ9xm/nPz7DE5u/M5Nt3Xq4WUTOT5Dv9sfaqim6HZ6hitpEEfrSKAryA1JsnF/YljSRo2MZCtx0LTw==", + "dev": true, + "dependencies": { + "resolvewithplus": "^1.0.2" + }, + "engines": { + "node": ">=14.16.0" + } + }, "node_modules/espree": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", @@ -19000,6 +19013,15 @@ "node": ">=4" } }, + "node_modules/resolvewithplus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/resolvewithplus/-/resolvewithplus-1.0.2.tgz", + "integrity": "sha512-UNvTSqWpe1lhKvXb+Qo97Q/dFUtA0kDuUG/gO7S+P9H/xYZ+A9pwlAbY6Dqp2tgAeQNdHmQG5MdNL6d9/FWo6Q==", + "dev": true, + "engines": { + "node": ">=12.16.0" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -27706,6 +27728,15 @@ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true }, + "esmock": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.0.6.tgz", + "integrity": "sha512-2EY6isqPxZ9xm/nPz7DE5u/M5Nt3Xq4WUTOT5Dv9sfaqim6HZ6hitpEEfrSKAryA1JsnF/YljSRo2MZCtx0LTw==", + "dev": true, + "requires": { + "resolvewithplus": "^1.0.2" + } + }, "espree": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", @@ -35476,6 +35507,12 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolvewithplus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/resolvewithplus/-/resolvewithplus-1.0.2.tgz", + "integrity": "sha512-UNvTSqWpe1lhKvXb+Qo97Q/dFUtA0kDuUG/gO7S+P9H/xYZ+A9pwlAbY6Dqp2tgAeQNdHmQG5MdNL6d9/FWo6Q==", + "dev": true + }, "responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", diff --git a/package.json b/package.json index 225e6df..4eaf31e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dist" ], "type": "module", - "main": "src/index.js", + "main": "dist/index.min.js", "repository": { "type": "git", "url": "https://github.com/ipfs-shipyard/ipfs-geoip" @@ -26,9 +26,9 @@ "lint": "aegir lint", "release": "aegir release", "build": "aegir build", - "test": "aegir test", + "test": "npm run test:node && npm run test:browser", "test:node": "aegir test --target node", - "test:browser": "aegir test --target browser", + "test:browser": "aegir test --target browser --files test/**/*.browser.spec.{js,cjs,mjs}", "generate": "node bin/generate.js" }, "dependencies": { @@ -47,6 +47,7 @@ "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "csv-parse": "^5.3.0", + "esmock": "^2.0.6", "gauge": "^4.0.4", "ipfs-http-client": "^58.0.1", "multihashes": "^4.0.3", @@ -64,9 +65,6 @@ "node": ">=16.0.0", "npm": ">=8.0.0" }, - "aegir": { - "tsRepo": false - }, "pre-commit": [ "lint" ], diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..5a796f9 --- /dev/null +++ b/src/constants.js @@ -0,0 +1 @@ +export const MAX_LOOKUP_RETRIES = 3 diff --git a/src/lookup.js b/src/lookup.js index 72563cc..5bf001b 100644 --- a/src/lookup.js +++ b/src/lookup.js @@ -1,9 +1,10 @@ import { default as memoize } from 'p-memoize' import ip from 'ip' -import * as dagCbor from '@ipld/dag-cbor' +import { decode as dagCborDecode } from '@ipld/dag-cbor' import { CID } from 'multiformats/cid' import fetch from 'cross-fetch' import { formatData } from './format.js' +import { MAX_LOOKUP_RETRIES } from './constants.js' export const GEOIP_ROOT = CID.parse('bafyreihnpl7ami7esahkfdnemm6idx4r2n6u3apmtcrxlqwuapgjsciihy') // b-tree version of GeoLite2-City-CSV_20221018 @@ -45,6 +46,27 @@ async function getRawBlock (ipfs, cid) { } } +/** + * Gets Obj and Block after retrying multiple times. + * + * @param {object|string} ipfs + * @param {CID} cid + * @param {number} numTry - this will be 1 for the first try and recurse till MAX_LOOKUP_RETRIES is reached. + * @returns {Promise<{obj, block}>} + */ +async function getObjAndBlockWithRetries (ipfs, cid, numTry = 1) { + try { + const block = await getRawBlock(ipfs, cid) + const obj = await dagCborDecode(block) + return { obj, block } + } catch (e) { + if (numTry < MAX_LOOKUP_RETRIES) { + return await getObjAndBlockWithRetries(ipfs, cid, numTry + 1) + } + throw e + } +} + /** * @param {object|string} ipfs * @param {CID} cid @@ -54,8 +76,7 @@ async function getRawBlock (ipfs, cid) { async function _lookup (ipfs, cid, lookfor) { let obj, block try { - block = await getRawBlock(ipfs, cid) - obj = await dagCbor.decode(block) + ({ obj, block } = await getObjAndBlockWithRetries(ipfs, cid)) } catch (e) { if (process?.env?.DEBUG || process?.env?.TEST) { if (!block) { diff --git a/test/format.spec.js b/test/format.browser.spec.js similarity index 100% rename from test/format.spec.js rename to test/format.browser.spec.js diff --git a/test/generate.spec.js b/test/generate.browser.spec.js similarity index 100% rename from test/generate.spec.js rename to test/generate.browser.spec.js diff --git a/test/lookup.spec.js b/test/lookup.browser.spec.js similarity index 100% rename from test/lookup.spec.js rename to test/lookup.browser.spec.js diff --git a/test/lookupMultiple.node.spec.js b/test/lookupMultiple.node.spec.js new file mode 100644 index 0000000..3ff93c1 --- /dev/null +++ b/test/lookupMultiple.node.spec.js @@ -0,0 +1,37 @@ +import { decode as dagCborDecode } from '@ipld/dag-cbor' +import esmock from 'esmock' +import { expect } from 'chai' + +describe('[Runner Node]: lookup via HTTP Gateway supporting application/vnd.ipld.raw responses', function () { + const ipfsGW = process?.env?.IPFS_GATEWAY || 'https://ipfs.io' + + it('looks up multiple times before failing', async () => { + let decodeCallCount = 0 + const rewiredGeoIp = await esmock('../src/index.js', {}, { + '@ipld/dag-cbor': { + decode: (...args) => { + decodeCallCount += 1 + if (decodeCallCount === 1) { + throw new Error('Decode Failed') + } + return dagCborDecode(...args) + } + } + }) + + const result = await rewiredGeoIp.lookup(ipfsGW, '66.6.44.4') + expect(decodeCallCount).to.be.greaterThan(1) + expect( + result + ).to.be.eql({ + country_name: 'USA', + country_code: 'US', + region_code: 'VA', + city: 'Ashburn', + postal_code: '20149', + latitude: 39.0469, + longitude: -77.4903, + planet: 'Earth' + }) + }) +})