From b30539673b2fd5a0724063c9d1eefb56353674d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Herv=C3=A9?= Date: Thu, 13 Jun 2019 10:10:12 +0200 Subject: [PATCH 1/2] Add lazy feature to slot --- README.md | 30 ++++++++++++ src/Slot.ts | 41 ++++++++++++++++ test/Slot.test.ts | 117 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da919f8..4bc73cd 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/Slot.ts b/src/Slot.ts index 137c1e5..70555b3 100644 --- a/src/Slot.ts +++ b/src/Slot.ts @@ -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 /** @@ -23,6 +24,7 @@ export interface Slot { // optional only when explicitly typed as such by the client. (requestData: RequestData): Promise on: (handler: Handler) => Unsubscribe + lazy: (connect: () => void, disconnect: () => void) => Unsubscribe } /** @@ -44,6 +46,13 @@ export function connectSlot(slotName: string, transports: Trans // This prevents `triggers` from firing *before* any far-end is listening. let remoteHandlersConnected = [] as Promise[] + // 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 => { @@ -62,6 +71,7 @@ export function connectSlot(slotName: string, transports: Trans t.onRemoteHandlerRegistered(slotName, (handler: Handler) => { 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`. @@ -71,6 +81,7 @@ export function connectSlot(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. @@ -91,6 +102,30 @@ export function connectSlot(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): Unsubscribe => { @@ -100,6 +135,9 @@ export function connectSlot(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 () => { @@ -110,6 +148,9 @@ export function connectSlot(slotName: string, transports: Trans if (ix !== -1) { handlers.splice(ix, 1) } + + // Call lazy disconnect callbacks if needed + if (!handlers.length) callLazyDisonnectCallbacks() } } diff --git a/test/Slot.test.ts b/test/Slot.test.ts index 0fc8aad..299c359 100644 --- a/test/Slot.test.ts +++ b/test/Slot.test.ts @@ -1,4 +1,5 @@ import 'should' +import { spy } from 'sinon' import { connectSlot } from './../src/Slot' import { TestChannel } from './TestChannel' @@ -73,6 +74,24 @@ describe('connectSlot()', () => { results.should.eql([55]) }) + it('should call lazy connect and disconnect', () => { + const broadcastBool = connectSlot('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', () => { @@ -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('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('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('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('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') + }) + }) + }) }) From 76481a942fbc54c25eb861919ae959679f6dcf5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Herv=C3=A9?= Date: Mon, 8 Jul 2019 09:59:25 +0200 Subject: [PATCH 2/2] Fix lodash vulnerability --- package.json | 3 ++- yarn.lock | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 993b8eb..491e759 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/yarn.lock b/yarn.lock index 3117bbf..a0b6ae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"