Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: propagate errors to single-response APIs #277

Merged
merged 5 commits into from
Apr 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions packages/aragon-rpc-messenger/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import jsonrpc from './jsonrpc'
import MessagePortMessage from './providers/MessagePortMessage'
import WindowMessage from './providers/WindowMessage'
import DevMessage from './providers/DevMessage'
import { first, filter } from 'rxjs/operators'
import { first, filter, map } from 'rxjs/operators'

export const providers = {
MessagePortMessage,
Expand Down Expand Up @@ -91,7 +91,14 @@ export default class Messenger {
const id = this.send(method, params)

return this.responses().pipe(
filter((message) => message.id === id)
filter((message) => message.id === id),
map((response) => {
if (response.error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to leave the response.error an error instance in https://github.com/aragon/aragon.js/blob/master/packages/aragon-rpc-messenger/src/jsonrpc.js#L19?

It seems like we are converting an error to a string in jsonrpc.js and back to an error here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primarily because sending an Error instance through postMessage() will fail: see things that don't work with the structured clone algorithm.

response.error = new Error(response.error)
}
return response
})
// Let callers handle errors themselves
)
}

Expand All @@ -104,7 +111,14 @@ export default class Messenger {
*/
sendAndObserveResponse (method, params = []) {
return this.sendAndObserveResponses(method, params).pipe(
first()
first(),
map((response) => {
// Emit an error if the response is an error
if (response.error) {
throw response.error
}
return response
})
)
}
}
71 changes: 62 additions & 9 deletions packages/aragon-rpc-messenger/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ test('should encode and send the request', t => {
})

test('should filter the incoming messages to responses only', (t) => {
t.plan(2)

// arrange
const busMock = new Subject()
const instance = new Messenger(null)
instance.bus = () => busMock
jsonrpcStub.isValidResponse = sinon.stub().returns(true)
// assert
t.plan(2)

// act
const messages = instance.responses()
messages.subscribe(value => {
Expand All @@ -87,13 +88,14 @@ test('should filter the incoming messages to responses only', (t) => {
})

test('should filter the incoming messages to requests only', (t) => {
t.plan(2)

// arrange
const busMock = new Subject()
const instance = new Messenger(null)
instance.bus = () => busMock
jsonrpcStub.isValidResponse = sinon.stub().returns(false)
// assert
t.plan(2)

// act
const messages = instance.requests()
messages.subscribe(value => {
Expand All @@ -105,38 +107,89 @@ test('should filter the incoming messages to requests only', (t) => {
})

test('should send and observe responses', (t) => {
t.plan(4)

// arrange
const instance = new Messenger(null)
sinon.stub(instance, 'send').returns(41)
sinon.stub(instance, 'responses').returns(new Subject())
const messages = instance.sendAndObserveResponses('sendEth')

// assert
t.plan(4)
messages.subscribe(value => t.is(value.data, 'thanks'))
t.is(instance.send.getCall(0).args[0], 'sendEth')
t.deepEqual(instance.send.getCall(0).args[1], [])

// act
instance.responses().next({ data: 'thanks', id: 41 })
instance.responses().next({ data: 'thanks', id: 41 })
})

test('should send and observe responses, even if errors are included', (t) => {
t.plan(6)

// arrange
const instance = new Messenger(null)
sinon.stub(instance, 'send').returns(41)
sinon.stub(instance, 'responses').returns(new Subject())
const messages = instance.sendAndObserveResponses('sendEth')
messages.subscribe(value => t.is(value.data, 'thanks'))

// assert
messages.subscribe(value => {
if (value.data) {
t.is(value.data, 'thanks')
} else if (value.error) {
t.true(value.error instanceof Error)
t.is(value.error.message, 'no thanks')
}
})
t.is(instance.send.getCall(0).args[0], 'sendEth')
t.deepEqual(instance.send.getCall(0).args[1], [])

// act
instance.responses().next({ data: 'thanks', id: 41 })
instance.responses().next({ error: 'no thanks', id: 41 })
instance.responses().next({ data: 'thanks', id: 41 })
})

test('should send and observe only the first response', (t) => {
t.plan(3)

// arrange
const instance = new Messenger(null)
sinon.stub(instance, 'sendAndObserveResponses').returns(new Subject())
// assert
t.plan(3)

// act
const messages = instance.sendAndObserveResponse('sendAnt')
// assert
messages.subscribe(value => t.is(value, 'first'))

t.is(instance.sendAndObserveResponses.getCall(0).args[0], 'sendAnt')
t.deepEqual(instance.sendAndObserveResponses.getCall(0).args[1], [])

instance.sendAndObserveResponses().next('first')
instance.sendAndObserveResponses().next('second')
instance.sendAndObserveResponses().next('third')
})

test('should send and observe only the first error', (t) => {
t.plan(4)

// arrange
const instance = new Messenger(null)
sinon.stub(instance, 'sendAndObserveResponses').returns(new Subject())
const messages = instance.sendAndObserveResponse('sendAnt')
// assert
messages.subscribe(
value => t.fail('should not have emitted any next values'),
error => {
t.true(error instanceof Error)
t.is(error.message, 'bad first')
}
)
t.is(instance.sendAndObserveResponses.getCall(0).args[0], 'sendAnt')
t.deepEqual(instance.sendAndObserveResponses.getCall(0).args[1], [])

// act
instance.sendAndObserveResponses().next({ error: new Error('bad first') })
instance.sendAndObserveResponses().next('second')
})
6 changes: 3 additions & 3 deletions packages/aragon-wrapper/src/rpc/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export function createResponse ({ request: { id } }, { error, value = null, kind
return {}
}

if (error) {
return { id, payload: error }
if (kind === 'E') {
return { id, payload: error || new Error() }
}

return { id, payload: value }
Expand All @@ -34,7 +34,7 @@ export function createRequestHandler (request$, requestType, handler) {
createResponse
),
// filter empty responses caused by Notifications of kind 'C'
filter((response) => response.payload !== undefined || response.error !== undefined)
filter((response) => response.payload !== undefined)
)
}

Expand Down
15 changes: 14 additions & 1 deletion packages/aragon-wrapper/src/rpc/handlers/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test.afterEach.always(() => {
})

test('should create a request handler', async (t) => {
t.plan(4)
t.plan(5)
// arrange
const requestStub = from([{
request: {
Expand All @@ -33,6 +33,12 @@ test('should create a request handler', async (t) => {
params: ['set', 'settings'],
value: { foo: 'bar' }
}
}, {
request: {
id: 'uuid5',
method: 'cache',
params: ['clear']
}
}, {
request: {
id: 'uuid6',
Expand All @@ -56,14 +62,21 @@ test('should create a request handler', async (t) => {
return Promise.reject(new Error(`no permissions to change ${request.params[1]}!!`))
}

if (request.params[0] === 'clear') {
return Promise.reject(new Error())
}

return Promise.resolve(`resolved ${request.params[1]}`)
}
// act
const result = createRequestHandler(requestStub, 'cache', handlerStub)
// assert
result.subscribe(value => {
if (value.id === 'uuid1') return t.is(value.payload, 'resolved settings')
if (value.id === 'uuid4') return t.true(value.payload instanceof Error)
if (value.id === 'uuid4') return t.is(value.payload.message, 'no permissions to change settings!!')
if (value.id === 'uuid5') return t.true(value.payload instanceof Error)
if (value.id === 'uuid5') return t.is(value.payload.message, '')
if (value.id === 'uuid6') return t.is(value.payload, 'resolved profile')
if (value.id === 'uuid8') return t.is(value.payload, null)
})
Expand Down