diff --git a/src/client.js b/src/client.js index 3e38c05..2185c85 100644 --- a/src/client.js +++ b/src/client.js @@ -164,7 +164,6 @@ export class Saturn { const options = Object.assign({}, this.opts, { format: 'car', jwt }, opts) const url = this.createRequestURL(cidPath, options) - const log = { url, startTime: new Date() @@ -218,6 +217,7 @@ export class Saturn { * @param {string} cidPath * @param {object} [opts={}] * @param {('car'|'raw')} [opts.format] + * @param {boolean} [opts.raceNodes] * @param {string} [opts.url] * @param {number} [opts.connectTimeout=5000] * @param {number} [opts.downloadTimeout=0] @@ -267,8 +267,13 @@ export class Saturn { if (fallbackCount > this.opts.fallbackLimit) { return } - const origins = nodes.slice(i, i + Saturn.defaultRaceCount) - opts.origins = origins + if (opts.raceNodes) { + const origins = nodes.slice(i, i + Saturn.defaultRaceCount).map((node) => node.url) + opts.origins = origins + } else { + opts.url = nodes[i].url + } + try { yield * fetchContent() return @@ -286,17 +291,17 @@ export class Saturn { /** * * @param {string} cidPath - * @param {boolean} race * @param {object} [opts={}] * @param {('car'|'raw')} [opts.format] + * @param {boolean} [opts.raceNodes] * @param {number} [opts.connectTimeout=5000] * @param {number} [opts.downloadTimeout=0] * @returns {Promise>} */ - async * fetchContent (cidPath, race = false, opts = {}) { + async * fetchContent (cidPath, opts = {}) { let res, controller, log - if (race) { + if (opts.raceNodes) { ({ res, controller, log } = await this.fetchCIDWithRace(cidPath, opts)) } else { ({ res, controller, log } = await this.fetchCID(cidPath, opts)) @@ -329,6 +334,7 @@ export class Saturn { * @param {string} cidPath * @param {object} [opts={}] * @param {('car'|'raw')} [opts.format] + * @param {boolean} [opts.raceNodes] * @param {number} [opts.connectTimeout=5000] * @param {number} [opts.downloadTimeout=0] * @returns {Promise} @@ -344,7 +350,7 @@ export class Saturn { * @returns {URL} */ createRequestURL (cidPath, opts) { - let origin = opts.url || opts.origins[0] || opts.cdnURL + let origin = opts.url || (opts.origins && opts.origins[0]) || opts.cdnURL origin = addHttpPrefix(origin) const url = new URL(`${origin}/ipfs/${cidPath}`) diff --git a/test/fallback.spec.js b/test/fallback.spec.js index cda4e68..b806eb2 100644 --- a/test/fallback.spec.js +++ b/test/fallback.spec.js @@ -128,7 +128,74 @@ describe('Client Fallback', () => { const saturn = new Saturn({ storage: mockStorage, clientKey: CLIENT_KEY, clientId: 'test' }) - const cid = saturn.fetchContentWithFallback('bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4', { url: 'node1.saturn.ms' }) + const cid = saturn.fetchContentWithFallback('bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4') + + const buffer = await concatChunks(cid) + const actualContent = String.fromCharCode(...buffer) + const expectedContent = 'hello world\n' + + assert.strictEqual(actualContent, expectedContent) + server.close() + mock.reset() + }) + + test('Content Fallback fetches a cid properly with race', async (t) => { + const handlers = [ + mockOrchHandler(5, TEST_DEFAULT_ORCH, 'saturn.ms'), + mockJWT(TEST_AUTH), + mockSaturnOriginHandler(TEST_ORIGIN_DOMAIN, 0, true), + ...mockNodesHandlers(5, TEST_ORIGIN_DOMAIN) + ] + const server = getMockServer(handlers) + server.listen(MSW_SERVER_OPTS) + + const expectedNodes = generateNodes(3, TEST_ORIGIN_DOMAIN) + + // Mocking storage object + const mockStorage = { + get: async (key) => expectedNodes, + set: async (key, value) => { return null } + } + t.mock.method(mockStorage, 'get') + t.mock.method(mockStorage, 'set') + + const saturn = new Saturn({ storage: mockStorage, clientKey: CLIENT_KEY, clientId: 'test' }) + // const origins = + + const cid = saturn.fetchContentWithFallback('bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4', { raceNodes: true }) + + const buffer = await concatChunks(cid) + const actualContent = String.fromCharCode(...buffer) + const expectedContent = 'hello world\n' + + assert.strictEqual(actualContent, expectedContent) + server.close() + mock.reset() + }) + + test('Content Fallback with race fetches from consecutive nodes on failure', async (t) => { + const handlers = [ + mockOrchHandler(5, TEST_DEFAULT_ORCH, 'saturn.ms'), + mockJWT(TEST_AUTH), + mockSaturnOriginHandler(TEST_ORIGIN_DOMAIN, 0, true), + ...mockNodesHandlers(5, TEST_ORIGIN_DOMAIN, 2) + ] + const server = getMockServer(handlers) + server.listen(MSW_SERVER_OPTS) + + const expectedNodes = generateNodes(5, TEST_ORIGIN_DOMAIN) + + // Mocking storage object + const mockStorage = { + get: async (key) => expectedNodes, + set: async (key, value) => { return null } + } + t.mock.method(mockStorage, 'get') + t.mock.method(mockStorage, 'set') + + const saturn = new Saturn({ storage: mockStorage, clientKey: CLIENT_KEY, clientId: 'test' }) + + const cid = saturn.fetchContentWithFallback('bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4', { raceNodes: true }) const buffer = await concatChunks(cid) const actualContent = String.fromCharCode(...buffer) diff --git a/test/test-utils.js b/test/test-utils.js index db26837..d51f2d3 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -123,14 +123,23 @@ export function mockJWT (authURL) { * * @param {number} count - amount of nodes to mock * @param {string} originDomain - saturn origin domain. + * @param {number} failures * @returns {RestHandler[]} */ -export function mockNodesHandlers (count, originDomain) { +export function mockNodesHandlers (count, originDomain, failures = 0) { + if (failures > count) { + throw Error('failures number cannot exceed node count') + } const nodes = generateNodes(count, originDomain) - const handlers = nodes.map((node) => { + const handlers = nodes.map((node, idx) => { const url = `${node.url}/ipfs/:cid` return rest.get(url, (req, res, ctx) => { + if (idx < failures) { + return res( + ctx.status(504) + ) + } const filepath = getFixturePath('hello.car') const fileContents = fs.readFileSync(filepath) return res(