diff --git a/src/Agent.js b/src/Agent.js index b4439fb..3e97640 100644 --- a/src/Agent.js +++ b/src/Agent.js @@ -58,12 +58,18 @@ export default class Agent { * @param {String} query Query parameters * @param {Object} form Form fields * @param {Object} files array of file names and file content - * @parma {Object} context the invocation context, describing the tool and project. + * @parma {Object} context the invocation context, describing the tool and project. A timeout for the request + * in milliseconds may be optionally specified. When not specified, the request does not timeout. * @return {Promise} A promise. fulfilled with {body, statusCode}, rejected with { statusCode, errorDescription, error, body } */ request({ uri, method, data = undefined, auth, query = undefined, form = undefined, files = undefined, context = undefined }) { const requestFiles = this._sanitizeFiles(files); - return this._request({ uri, method, data, auth, query, form, context, files: requestFiles }); + const timeout = context && context.timeout; + if (timeout) { + context = Object.assign({}, context); + delete context['timeout']; + } + return this._request({ uri, method, data, auth, query, form, timeout, context, files: requestFiles }); } /** @@ -75,11 +81,13 @@ export default class Agent { * @param {String} query Query parameters * @param {Object} form Form fields * @param {Object} files array of file names and file content + * @param {Number} timeout the number of milliseconds of no response after which the request should timeout. + * When undefined, the request is not timed out. * @param {Object} context the invocation context * @return {Promise} A promise. fulfilled with {body, statusCode}, rejected with { statusCode, errorDescription, error, body } */ - _request({ uri, method, data, auth, query, form, files, context }) { - const req = this._buildRequest({ uri, method, data, auth, query, form, context, files }); + _request({ uri, method, data, auth, query, form, files, timeout, context }) { + const req = this._buildRequest({ uri, method, data, auth, query, form, timeout, context, files }); return this._promiseResponse(req); } @@ -125,7 +133,7 @@ export default class Agent { }); } - _buildRequest({ uri, method, data, auth, query, form, files, context, makerequest=request }) { + _buildRequest({ uri, method, data, auth, query, form, files, timeout, context, makerequest=request }) { const req = makerequest(method, uri); if (this.prefix) { req.use(this.prefix); @@ -137,6 +145,9 @@ export default class Agent { if (query) { req.query(query); } + if (timeout!==undefined) { + req.timeout(timeout); + } if (files) { for (let [name, file] of Object.entries(files)) { req._getFormData().append(name, file.data, { diff --git a/src/Defaults.js b/src/Defaults.js index 7a2e8a5..690f174 100644 --- a/src/Defaults.js +++ b/src/Defaults.js @@ -2,5 +2,6 @@ export default { baseUrl: 'https://api.particle.io', clientSecret: 'particle-api', clientId: 'particle-api', - tokenDuration: 7776000 // 90 days + tokenDuration: 7776000, // 90 days + timeout: 60000 }; diff --git a/src/Particle.js b/src/Particle.js index cdb47af..a373c4c 100644 --- a/src/Particle.js +++ b/src/Particle.js @@ -520,9 +520,9 @@ class Particle { uri += `orgs/${org}/`; } - if (product) { - uri += `products/${product}/`; - } + if (product) { + uri += `products/${product}/`; + } if (deviceId) { uri += 'devices/'; @@ -1002,7 +1002,7 @@ class Particle { const uri = product ? `/v1/products/${product}/clients` : '/v1/clients'; const data = { name, type, redirect_uri, scope }; return this.post(uri, data, auth, context); - } + } /** * Update an OAuth client @@ -1090,8 +1090,8 @@ class Particle { }, context, auth - }); - } + }); + } /** * Get information about a product firmware version @@ -1103,7 +1103,7 @@ class Particle { */ getProductFirmware({ version, product, auth, context }) { return this.get(`/v1/products/${product}/firmware/${version}`, auth, undefined, context); - } + } /** * Update information for a product firmware version @@ -1133,9 +1133,9 @@ class Particle { const req = request('get', uri); req.use(this.prefix); this.headers(req, auth); - if (this.debug) { - this.debug(req); - } + if (this.debug) { + this.debug(req); + } return req; } @@ -1150,7 +1150,7 @@ class Particle { releaseProductFirmware({ version, product, auth, context }) { const uri = `/v1/products/${product}/firmware/release`; return this.put(uri, { version }, auth, context); - } + } /** * List product team members @@ -1161,7 +1161,7 @@ class Particle { */ listTeamMembers({ product, auth, context }) { return this.get(`/v1/products/${product}/team`, auth, undefined, context); - } + } /** * Invite Particle user to a product team @@ -1185,7 +1185,7 @@ class Particle { */ removeTeamMember({ username, product, auth, context }) { return this.delete(`/v1/products/${product}/team/${username}`, undefined, auth, context); - } + } /** * API URI to access a device @@ -1217,7 +1217,7 @@ class Particle { put(uri, data, auth, context) { context = this._buildContext(context); return this.agent.put(uri, data, auth, context); - } + } delete(uri, data, auth, context) { context = this._buildContext(context); @@ -1231,7 +1231,7 @@ class Particle { client(options = {}) { return new Client(Object.assign({ api: this }, options)); - } +} } // Aliases for backwards compatibility diff --git a/test/Agent.spec.js b/test/Agent.spec.js index 524aeb0..abebc89 100644 --- a/test/Agent.spec.js +++ b/test/Agent.spec.js @@ -144,7 +144,6 @@ describe('Agent', () => { expect(sut._applyContext).to.not.be.called; }); - it('should invoke authorize with the request and auth', () => { const sut = new Agent(); sut.prefix = undefined; @@ -243,6 +242,25 @@ describe('Agent', () => { expect(field).to.be.calledWith('form1', 'value1'); expect(field).to.be.calledWith('form2', 'value2'); }); + + it('sets the timeout on the request when defined', () => { + const sut = new Agent(); + sut.prefix = undefined; + const req = { timeout: sinon.stub() }; + const makerequest = sinon.stub().returns(req); + sut._buildRequest({timeout: 123, makerequest}); + expect(req.timeout).calledOnce.calledWith(123); + }); + + it('does not set the timeout on the request when not defined', () => { + const sut = new Agent(); + sut.prefix = undefined; + const req = { timeout: sinon.stub() }; + const makerequest = sinon.stub().returns(req); + sut._buildRequest({makerequest}); + expect(req.timeout).not.called; + }); + }); it('sanitizes files from a request', () => { @@ -258,7 +276,19 @@ describe('Agent', () => { expect(result).to.be.equal('request_result'); expect(sut._sanitizeFiles).calledOnce.calledWith(sinon.match.same(files)); expect(sut._request).calledOnce.calledWith({uri: 'abc', auth: undefined, method: 'post', data: '123', query: 'all', form:form, - files:sanitizedFiles, context: undefined}); + files:sanitizedFiles, timeout: undefined, context: undefined}); + }); + + describe('timeout', () => { + it('sets the timeout from the context', () => { + const sut = new Agent(); + sut._request = sinon.stub(); + const context = { timeout: 123, foo: 'bar' }; + sut.request({context}); + expect(sut._request).calledOnce.calledWith({uri: undefined, method: undefined, + auth: undefined, data: undefined, files: undefined, form: undefined, query: undefined, timeout: 123, + context: { foo: 'bar' } }); + }); }); it('uses default arguments for request', () => { @@ -269,20 +299,21 @@ describe('Agent', () => { const result = sut.request({uri: 'abc', method:'post'}); expect(result).to.equal('123'); expect(sut._request).calledOnce.calledWith({uri: 'abc', method:'post', - auth: undefined, data: undefined, files: undefined, form: undefined, query: undefined, context: undefined }); + auth: undefined, data: undefined, files: undefined, form: undefined, query: undefined, timeout: undefined, context: undefined }); }); it('builds and sends the request', () => { const sut = new Agent(); const buildRequest = sinon.stub(); const promiseResponse = sinon.stub(); + const context = {}; sut._buildRequest = buildRequest; sut._promiseResponse = promiseResponse; buildRequest.returns('arequest'); promiseResponse.returns('promise'); const requestArgs = {uri:'uri', method:'method', data:'data', auth:'auth', query: 'query', - form: 'form', files: 'files', context}; + form: 'form', files: 'files', timeout: undefined, context}; const result = sut._request(requestArgs); expect(result).to.be.equal('promise'); expect(buildRequest).calledWith(requestArgs);