Skip to content

Commit

Permalink
Merge pull request #10 from Alexandre-Herve/lazy-slot
Browse files Browse the repository at this point in the history
Implement lazy slot capabilities
  • Loading branch information
Alexandre-Herve authored Jul 9, 2019
2 parents 64e4169 + 76481a9 commit 9f09836
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 5 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,36 @@ EventBus.multiply.on(({a, b}) => {
})
```

### Lazy callbacks

Slots expose a `lazy` method that will allow you to call a "connect" callback when a first
client connects to the slot, and a "disconnect" callback when the last client disconnect.

Remote or local clients are considered equally. If a client was already connected to the slot
at the time when `lazy` is called, the "connect" callback is called immediately.

```typescript
const connect = () => {
console.log('Someone somewhere has begun listening to the slot with `.on`.')
}

const disconnect = () => {
console.log('No one is listening to the slot anymore.')
}

const disconnectLazy = EventBus.ping.lazy(connect, disconnect)

const unsubscribe = EventBus.ping().on(() => { })
// console output: 'Someone somewhere has begun listening to the slot with `.on`.'

unsubscribe()
// console output: 'No one is listening to the slot anymore.'

// Remove the callbacks.
// "disconnect" is called one last time if there were subscribers left on the slot.
disconnectLazy()
```

### Syntactic sugar

You can combine events from different sources.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"event-bus"
],
"resolutions": {
"js-yaml": ">=3.13.1"
"js-yaml": ">=3.13.1",
"lodash": ">=4.17.11"
},
"license": "Apache-2.0"
}
41 changes: 41 additions & 0 deletions src/Slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const signalNotConnected = () => { throw new Error('Slot not connected') }
const FAKE_SLOT: any = () => signalNotConnected()
FAKE_SLOT.on = signalNotConnected

export type SimpleCallback = () => void
export type Unsubscribe = () => void

/**
Expand All @@ -23,6 +24,7 @@ export interface Slot<RequestData=null, ResponseData=void> {
// optional only when explicitly typed as such by the client.
(requestData: RequestData): Promise<ResponseData>
on: (handler: Handler<RequestData, ResponseData>) => Unsubscribe
lazy: (connect: () => void, disconnect: () => void) => Unsubscribe
}

/**
Expand All @@ -44,6 +46,13 @@ export function connectSlot<T=void, T2=void>(slotName: string, transports: Trans
// This prevents `triggers` from firing *before* any far-end is listening.
let remoteHandlersConnected = [] as Promise<any>[]

// Lazy
const lazyConnectCallbacks: SimpleCallback[] = []
const lazyDisonnectCallbacks: SimpleCallback[] = []

const callLazyConnectCallbacks = () => lazyConnectCallbacks.forEach(c => c())
const callLazyDisonnectCallbacks = () => lazyDisonnectCallbacks.forEach(c => c())

// Signal to all transports that we will accept handlers for this slotName
transports.forEach(t => {

Expand All @@ -62,6 +71,7 @@ export function connectSlot<T=void, T2=void>(slotName: string, transports: Trans

t.onRemoteHandlerRegistered(slotName, (handler: Handler<any, any>) => {
handlers.push(handler)
if (handlers.length === 1) callLazyConnectCallbacks()

// We signal that the transport is ready for this slot by resolving the
// promise stored in `remoteHandlersConnected`.
Expand All @@ -71,6 +81,7 @@ export function connectSlot<T=void, T2=void>(slotName: string, transports: Trans
t.onRemoteHandlerUnregistered(slotName, handler => {
const handlerIndex = handlers.indexOf(handler)
handlers.splice(handlerIndex, 1)
if (handlers.length === 0) callLazyDisonnectCallbacks()

// When the channel disconnects we also need to remove the
// promise blocking the trigger.
Expand All @@ -91,6 +102,30 @@ export function connectSlot<T=void, T2=void>(slotName: string, transports: Trans
Promise.all(remoteHandlersConnected).then(() => callHandlers(data, handlers)) :
callHandlers(data, handlers)

trigger.lazy = (
firstClientConnectCallback: SimpleCallback,
lastClientDisconnectCallback: SimpleCallback
): Unsubscribe => {

lazyConnectCallbacks.push(firstClientConnectCallback)
lazyDisonnectCallbacks.push(lastClientDisconnectCallback)

// Call connect callback
if (handlers.length > 0) firstClientConnectCallback()

return () => {
// Call disconnect callback
if (handlers.length > 0) lastClientDisconnectCallback()

// Stop lazy connect and disconnect process
const connectIx = lazyConnectCallbacks.indexOf(firstClientConnectCallback)
if (connectIx > -1) lazyConnectCallbacks.splice(connectIx, 1)

const disconnectIx = lazyDisonnectCallbacks.indexOf(lastClientDisconnectCallback)
if (disconnectIx > -1) lazyDisonnectCallbacks.splice(disconnectIx, 1)
}
}

// Called when a client subscribes to the slot (with `Slot.on()`)
trigger.on = (handler: Handler<any, any>): Unsubscribe => {

Expand All @@ -100,6 +135,9 @@ export function connectSlot<T=void, T2=void>(slotName: string, transports: Trans
// Store this handler
handlers.push(handler)

// Call lazy connect callbacks if needed
if (handlers.length === 1) callLazyConnectCallbacks()

// Return the unsubscription function
return () => {

Expand All @@ -110,6 +148,9 @@ export function connectSlot<T=void, T2=void>(slotName: string, transports: Trans
if (ix !== -1) {
handlers.splice(ix, 1)
}

// Call lazy disconnect callbacks if needed
if (!handlers.length) callLazyDisonnectCallbacks()
}

}
Expand Down
117 changes: 116 additions & 1 deletion test/Slot.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'should'
import { spy } from 'sinon'

import { connectSlot } from './../src/Slot'
import { TestChannel } from './TestChannel'
Expand Down Expand Up @@ -73,6 +74,24 @@ describe('connectSlot()', () => {
results.should.eql([55])
})

it('should call lazy connect and disconnect', () => {
const broadcastBool = connectSlot<boolean>('broadcastBool', [])

const connect = spy()
const disconnect = spy()

broadcastBool.lazy(connect, disconnect)

const unsubscribe = broadcastBool.on(() => {})

if (!connect.called) throw new Error('connect should have been called')
if (disconnect.called) throw new Error('disconnect should not have been called')

unsubscribe()

if (!disconnect.called) throw new Error('disconnect should have been called')
})

})

context('with local and remote handlers', () => {
Expand Down Expand Up @@ -112,6 +131,102 @@ describe('connectSlot()', () => {
await triggerPromise
})

})
describe('lazy', () => {
it('should call connect and disconnect', () => {
const { channel: channel1, transport: transport1 } = makeTestTransport()
const { channel: channel2, transport: transport2 } = makeTestTransport()
const broadcastBool = connectSlot<boolean>('broadcastBool', [transport1, transport2])

const connect = spy()
const disconnect = spy()

broadcastBool.lazy(connect, disconnect)

// Simulate two remote connextions to the slot
channel1.fakeReceive({ type: 'handler_registered', slotName: 'broadcastBool'})
channel2.fakeReceive({ type: 'handler_registered', slotName: 'broadcastBool'})

// Connect should have been called once
if (!connect.calledOnce) throw new Error('connect should have been called once')

// Disconnect should not have been called
if (disconnect.called) throw new Error('disconnect should not have been called')

// Disconnect first remote client
channel1.fakeReceive({ type: 'handler_unregistered', slotName: 'broadcastBool'})

// Disconnect should not have been called
if (disconnect.called) throw new Error('disconnect should not have been called')

// Disconnect second remote client
channel2.fakeReceive({ type: 'handler_unregistered', slotName: 'broadcastBool'})

// Disconnect should have been called once
if (!disconnect.calledOnce) throw new Error('disconnect should have been called once')
})

it('should support multiple lazy calls', () => {
const { channel: channel1, transport: transport1 } = makeTestTransport()

const connect1 = spy()
const disconnect1 = spy()

const connect2 = spy()
const disconnect2 = spy()

const broadcastBool = connectSlot<boolean>('broadcastBool', [transport1])

broadcastBool.lazy(connect1, disconnect1)
broadcastBool.lazy(connect2, disconnect2)

channel1.fakeReceive({ type: 'handler_registered', slotName: 'broadcastBool'})

// Connects should have been called once
if (!connect1.calledOnce) throw new Error('connect1 should have been called once')
if (!connect2.calledOnce) throw new Error('connect1 should have been called once')

channel1.fakeReceive({ type: 'handler_unregistered', slotName: 'broadcastBool'})

// Disonnects should have been called once
if (!disconnect1.calledOnce) throw new Error('connect1 should have been called once')
if (!disconnect2.calledOnce) throw new Error('connect1 should have been called once')
})

it('should call connect if transport was registered before lazy was called', () => {
const { channel: channel1, transport: transport1 } = makeTestTransport()
const broadcastBool = connectSlot<boolean>('broadcastBool', [transport1])

const connect = spy()

// Register remote handler *before* calling lazy
channel1.fakeReceive({ type: 'handler_registered', slotName: 'broadcastBool'})

broadcastBool.lazy(connect, () => {})

// Connect should have been called once
if (!connect.calledOnce) throw new Error('connect should have been called once')
})

it('should call disconnect on unsubscribe when remote client is connected', () => {
const { channel: channel1, transport: transport1 } = makeTestTransport()
const broadcastBool = connectSlot<boolean>('broadcastBool', [transport1])

const disconnect = spy()

// Register remote handler
channel1.fakeReceive({ type: 'handler_registered', slotName: 'broadcastBool'})

// Connect lazy
const unsubscribe = broadcastBool.lazy(() => {}, disconnect)

// Disonnect should not have been called
if (disconnect.called) throw new Error('disconnect should not have been called')

unsubscribe()

// Disconnect should have been called once
if (!disconnect.calledOnce) throw new Error('disconnect should have been called once')
})
})
})
})
7 changes: 4 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2095,9 +2095,10 @@ lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"

lodash@^4.17.4:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@>=4.17.11, lodash@^4.17.4:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==

lolex@^2.2.0, lolex@^2.3.2:
version "2.3.2"
Expand Down

0 comments on commit 9f09836

Please sign in to comment.