Skip to content

Commit

Permalink
feat(Realtime): Allow unsubscibe with not all config
Browse files Browse the repository at this point in the history
  • Loading branch information
kosssi committed May 7, 2019
1 parent 5592470 commit 47c582a
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 103 deletions.
11 changes: 8 additions & 3 deletions packages/cozy-realtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ or
## Example

```js
import CozyRealtime, { EVENT_CREATED } from 'cozy-realtime'
import CozyRealtime, { EVENT_CREATED, EVENT_UPDATED } from 'cozy-realtime'

const realtime = new CozyRealtime(cozyClient)
const type = 'io.cozy.accounts'
const id = 'document_id'
const handlerCreate = accounts => {
console.log(`A new 'io.cozy.accounts' is created with id '${accounts._id}'.`)
}
Expand All @@ -54,12 +55,16 @@ const handlerUpdate = accounts => {

// To subscribe
await realtime.onCreate({ type }, handlerCreate)
await realtime.onUpdate({ type }, handlerUpdate)
await realtime.onUpdate({ type, id }, handlerUpdate)

// To unsubscribe event for a type, an event name and an handler
await realtime.unsubscribe({ type, eventName: EVENT_CREATED }, handlerCreate)
// To unsubscribe all events for a type, id and an event name
await realtime.unsubscribe({ type, eventName: EVENT_UPDATED, id })
// To unsubscribe all events for a type and an event name
await realtime.unsubscribe({ type, eventName: EVENT_CREATED })
await realtime.unsubscribe({ type, eventName: EVENT_UPDATED })
// To unsubscribe all event for a type and id
await realtime.unsubscribe({ type, id })
// To unsubscribe all event for a type
await realtime.unsubscribe({ type })
// To unsubscribe all
Expand Down
69 changes: 37 additions & 32 deletions packages/cozy-realtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,6 @@ class CozyRealtime {
this._socket.updateAuthentication(token)
}

/**
* Launch close socket if no handler
*/
_resetSocketIfNoHandler() {
if (this._numberOfHandlers === 0) {
this._resetSocket()
}
}

/**
* Reset socket
*/
Expand All @@ -228,9 +219,8 @@ class CozyRealtime {
* @return {Promise} Promise that the message has been sent.
*/
_subscribe(config, handler) {
const key = generateKey(config)

return new Promise(resolve => {
const key = generateKey(config)
this.on(key, handler)
this._numberOfHandlers++

Expand All @@ -239,6 +229,42 @@ class CozyRealtime {
})
}

/**
* Remove the given handler from the list of handlers for given
* doctype/document and event.
*
* @param {String} type Document doctype to unsubscribe from
* @param {String} id Document id to unsubscribe from
* @param {String} eventName Event to unsubscribe from
* @param {Function} handler Function to call when an event of the
* given type on the given doctype or document is received from stack.
*/
unsubscribe(config, handler = undefined) {
const keys = [generateKey(config)]

if (!config.eventName) {
keys.push(generateKey({ ...config, eventName: EVENT_CREATED }))
keys.push(generateKey({ ...config, eventName: EVENT_UPDATED }))
keys.push(generateKey({ ...config, eventName: EVENT_DELETED }))
}

for (const key of keys) {
if (this._events[key]) {
if (handler) {
this._numberOfHandlers--
this.removeListener(key, handler)
} else {
this._numberOfHandlers -= this._events[key].length
this.removeAllListeners(key)
}
}
}

if (this._numberOfHandlers === 0) {
this._resetSocket()
}
}

_validateConfig(name, config, authorize) {
const notAllowed = Object.keys(config).filter(k => !authorize.includes(k))
if (notAllowed.length > 0) {
Expand All @@ -264,27 +290,6 @@ class CozyRealtime {
return this._subscribe({ ...config, eventName: EVENT_DELETED }, handler)
}

/**
* Remove the given handler from the list of handlers for given
* doctype/document and event.
*
* @param {String} type Document doctype to unsubscribe from
* @param {String} id Document id to unsubscribe from
* @param {String} eventName Event to unsubscribe from
* @param {Function} handler Function to call when an event of the
* given type on the given doctype or document is received from stack.
*/
unsubscribe(config, handler) {
const key = generateKey(config)

return new Promise(resolve => {
this._socket.once('close', resolve)
this.removeListener(key, handler)
this._numberOfHandlers--
this._resetSocketIfNoHandler()
})
}

/**
* Unsubscibe all handlers and close socket
*/
Expand Down
184 changes: 116 additions & 68 deletions packages/cozy-realtime/src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,102 +45,149 @@ describe('Realtime', () => {
const id = 'doc_id'
const fakeDoc = { _id: id, title: 'title1' }

it('should launch handler when document is created', async done => {
await realtime.onCreate({ type }, doc => {
expect(doc).toEqual(fakeDoc)
done()
describe('subscribe', () => {
it('should launch handler when document is created', async done => {
await realtime.onCreate({ type }, doc => {
expect(doc).toEqual(fakeDoc)
done()
})

cozyStack.emitMessage(type, fakeDoc, 'CREATED')
})

cozyStack.emitMessage(type, fakeDoc, 'CREATED')
})
it('should throw an error when config has id for onCreate', () => {
expect(() => realtime.onCreate({ type, id: 'my_id' }, () => {})).toThrow()
})

it('should throw an error when config has id for onCreate', () => {
expect(() => realtime.onCreate({ type, id: 'my_id' }, () => {})).toThrow()
})
it('should launch handler when document is updated', async done => {
await realtime.onUpdate({ type }, doc => {
expect(doc).toEqual(fakeDoc)
done()
})

it('should launch handler when document is updated', async done => {
await realtime.onUpdate({ type }, doc => {
expect(doc).toEqual(fakeDoc)
done()
cozyStack.emitMessage(type, fakeDoc, 'UPDATED')
})

cozyStack.emitMessage(type, fakeDoc, 'UPDATED')
})
it('should launch handler when document with id is updated', async done => {
await realtime.onUpdate({ type, id: fakeDoc._id }, doc => {
expect(doc).toEqual(fakeDoc)
done()
})

it('should launch handler when document with id is updated', async done => {
await realtime.onUpdate({ type, id: fakeDoc._id }, doc => {
expect(doc).toEqual(fakeDoc)
done()
cozyStack.emitMessage(type, fakeDoc, 'UPDATED', fakeDoc._id)
})

cozyStack.emitMessage(type, fakeDoc, 'UPDATED', fakeDoc._id)
})
it('should launch handler when document is deleted', async done => {
await realtime.onDelete({ type }, doc => {
expect(doc).toEqual(fakeDoc)
done()
})

it('should launch handler when document is deleted', async done => {
await realtime.onDelete({ type }, doc => {
expect(doc).toEqual(fakeDoc)
done()
cozyStack.emitMessage(type, fakeDoc, 'DELETED')
})

cozyStack.emitMessage(type, fakeDoc, 'DELETED')
})
it('should launch handler when document with id is deleted', async done => {
await realtime.onDelete({ type, id: fakeDoc._id }, doc => {
expect(doc).toEqual(fakeDoc)
done()
})

it('should launch handler when document with id is deleted', async done => {
await realtime.onDelete({ type, id: fakeDoc._id }, doc => {
expect(doc).toEqual(fakeDoc)
done()
cozyStack.emitMessage(type, fakeDoc, 'DELETED', fakeDoc._id)
})

cozyStack.emitMessage(type, fakeDoc, 'DELETED', fakeDoc._id)
it('should relauch socket subscribe after an error', async () => {
const handler = jest.fn()
await realtime.onCreate({ type }, handler)
realtime._retryDelay = 100

expect(realtime._socket.isOpen()).toBe(true)
cozyStack.simulate('error')
expect(realtime._socket.isOpen()).toBe(false)
cozyStack.emitMessage(type, fakeDoc, 'CREATED')
expect(handler.mock.calls.length).toBe(0)

await pause(200)
expect(realtime._socket.isOpen()).toBe(true)
cozyStack.emitMessage(type, fakeDoc, 'CREATED')
expect(handler.mock.calls.length).toBe(1)
})
})

it('should unsubscribe event', async () => {
const handler = jest.fn()
await realtime.onCreate({ type }, handler)
expect(realtime._socket.isOpen()).toBe(true)
await realtime.unsubscribe({ type }, handler)
expect(realtime._socket.isOpen()).toBe(false)
})
describe('unsubscribe', () => {
let handlerCreate, handlerUpdate, handlerDelete

it('should relauch socket subscribe after an error', async () => {
const handler = jest.fn()
beforeEach(async () => {
handlerCreate = jest.fn()
handlerUpdate = jest.fn()
handlerDelete = jest.fn()
await realtime.onCreate({ type }, handlerCreate)
await realtime.onUpdate({ type }, handlerUpdate)
await realtime.onDelete({ type }, handlerDelete)
await realtime.onCreate({ type: 'io.cozy.accounts' }, handlerCreate)
})

await realtime.onCreate({ type }, handler)
realtime._retryDelay = 100
afterEach(() => {
realtime.unsubscribeAll()
})

expect(realtime._socket.isOpen()).toBe(true)
cozyStack.simulate('error')
expect(realtime._socket.isOpen()).toBe(false)
cozyStack.emitMessage(type, fakeDoc, 'CREATED')
expect(handler.mock.calls.length).toBe(0)
it('should unsubscribe handlerCreate with type, eventName and handler', () => {
expect(realtime._socket.isOpen()).toBe(true)
expect(realtime._numberOfHandlers).toBe(4)
realtime.unsubscribe({ type, eventName: EVENT_CREATED }, handlerCreate)
expect(realtime._numberOfHandlers).toBe(3)
expect(realtime._socket.isOpen()).toBe(true)
})

await pause(200)
expect(realtime._socket.isOpen()).toBe(true)
cozyStack.emitMessage(type, fakeDoc, 'CREATED')
expect(handler.mock.calls.length).toBe(1)
it('should unsubscribe handlerCreate with type and eventName', () => {
expect(realtime._socket.isOpen()).toBe(true)
expect(realtime._numberOfHandlers).toBe(4)
realtime.unsubscribe({ type, eventName: EVENT_CREATED })
expect(realtime._numberOfHandlers).toBe(3)
expect(realtime._socket.isOpen()).toBe(true)
})

it('should unsubscribe handlerCreate with type', () => {
expect(realtime._socket.isOpen()).toBe(true)
expect(realtime._numberOfHandlers).toBe(4)
realtime.unsubscribe({ type })
expect(realtime._numberOfHandlers).toBe(1)
expect(realtime._socket.isOpen()).toBe(true)
})

it('should unsubscribe all events', () => {
expect(realtime._socket.isOpen()).toBe(true)
expect(realtime._numberOfHandlers).toBe(4)
realtime.unsubscribeAll()
expect(realtime._numberOfHandlers).toBe(0)
expect(realtime._socket.isOpen()).toBe(false)
})
})

it('should emit error when retry limit is exceeded', async done => {
realtime._retryLimit = 0
describe('events', () => {
it('should emit error when retry limit is exceeded', async done => {
realtime._retryLimit = 0

realtime.on('error', () => done())
const handler = jest.fn()
realtime.on('error', () => done())
const handler = jest.fn()

await realtime.onCreate({ type }, handler)
expect(realtime._socket.isOpen()).toBe(true)
cozyStack.simulate('error')
await realtime.onCreate({ type }, handler)
expect(realtime._socket.isOpen()).toBe(true)
cozyStack.simulate('error')
})
})

it('should update socket authentication when client login', async () => {
realtime._socket.updateAuthentication = jest.fn()
COZY_CLIENT.emit('login')
expect(realtime._socket.updateAuthentication.mock.calls.length).toBe(1)
})
describe('authentication', () => {
it('should update socket authentication when client login', async () => {
realtime._socket.updateAuthentication = jest.fn()
COZY_CLIENT.emit('login')
expect(realtime._socket.updateAuthentication.mock.calls.length).toBe(1)
})

it('should update socket authentication when client token refreshed ', async () => {
realtime._socket.updateAuthentication = jest.fn()
COZY_CLIENT.emit('login')
expect(realtime._socket.updateAuthentication.mock.calls.length).toBe(1)
it('should update socket authentication when client token refreshed ', async () => {
realtime._socket.updateAuthentication = jest.fn()
COZY_CLIENT.emit('login')
expect(realtime._socket.updateAuthentication.mock.calls.length).toBe(1)
})
})
})

Expand Down Expand Up @@ -181,6 +228,7 @@ describe('getWebSocketToken', () => {
fakeCozyClient.stackClient.token.token = 'token2'
expect(getWebSocketToken(fakeCozyClient)).toBe('token2')
})

it('should return oauth token from cozyClient', () => {
const fakeCozyClient = { stackClient: { token: {} } }
fakeCozyClient.stackClient.token.accessToken = COZY_TOKEN
Expand Down

0 comments on commit 47c582a

Please sign in to comment.