From 9afdf265077d5b237c5523e93c211456caa99279 Mon Sep 17 00:00:00 2001 From: Brian Diehr Date: Fri, 9 Sep 2022 15:10:03 -0700 Subject: [PATCH] feat(core): Add viewportManager to orchestrate viewport syncing within groups --- .../viewportManager/viewportManager.spec.ts | 73 +++++++++++++++++++ .../src/viewportManager/viewportManager.ts | 56 ++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 packages/core/src/viewportManager/viewportManager.spec.ts create mode 100644 packages/core/src/viewportManager/viewportManager.ts diff --git a/packages/core/src/viewportManager/viewportManager.spec.ts b/packages/core/src/viewportManager/viewportManager.spec.ts new file mode 100644 index 000000000..98fe8d051 --- /dev/null +++ b/packages/core/src/viewportManager/viewportManager.spec.ts @@ -0,0 +1,73 @@ +import { viewportManager } from './viewportManager'; + +beforeEach(() => { + viewportManager.reset(); +}); + +const VIEWPORT = { duration: '2m' }; + +it('subscribes to a new viewport group and returned an undefined viewport', () => { + const { viewport } = viewportManager.subscribe('some-group', () => {}); + expect(viewport).toBeUndefined(); +}); + +it('broadcast updates to viewport group', () => { + const listener = jest.fn(); + viewportManager.subscribe('some-group', listener); + viewportManager.update('some-group', VIEWPORT); + expect(listener).toHaveBeenLastCalledWith(VIEWPORT); +}); + +it('returns current viewport for group is returned upon initial subscription', () => { + const listener = jest.fn(); + viewportManager.update('some-group', VIEWPORT); + const { viewport } = viewportManager.subscribe('some-group', listener); + + expect(viewport).toBe(VIEWPORT); +}); + +it('returns no viewport is returned on initial subscription when reset is called', () => { + const listener = jest.fn(); + viewportManager.update('some-group', VIEWPORT); + viewportManager.reset(); + const { viewport } = viewportManager.subscribe('some-group', listener); + + expect(viewport).toBeUndefined(); +}); + +it('does not broadcast viewport updates to different viewport groups ', () => { + const listener = jest.fn(); + viewportManager.subscribe('some-group', listener); + viewportManager.update('some-other-group', VIEWPORT); + expect(listener).not.toHaveBeenCalled(); +}); + +it('broadcasts viewports to multiple listeners', () => { + const listener = jest.fn(); + const listener2 = jest.fn(); + viewportManager.subscribe('some-group', listener); + viewportManager.subscribe('some-group', listener2); + viewportManager.update('some-group', VIEWPORT); + + expect(listener).toHaveBeenLastCalledWith(VIEWPORT); + expect(listener2).toHaveBeenLastCalledWith(VIEWPORT); +}); + +it('does not broadcast updates to a unsubscribed listener', () => { + const listener = jest.fn(); + const { unsubscribe } = viewportManager.subscribe('some-group', listener); + + unsubscribe(); + viewportManager.update('some-group', VIEWPORT); + expect(listener).not.toHaveBeenCalled(); +}); + +it('does not broadcast updates to a listener after reset is called', () => { + const listener = jest.fn(); + viewportManager.subscribe('some-group', listener); + + viewportManager.reset(); + viewportManager.update('some-group', VIEWPORT); + + expect(listener).not.toHaveBeenCalled(); +}); diff --git a/packages/core/src/viewportManager/viewportManager.ts b/packages/core/src/viewportManager/viewportManager.ts new file mode 100644 index 000000000..866645cdc --- /dev/null +++ b/packages/core/src/viewportManager/viewportManager.ts @@ -0,0 +1,56 @@ +import { MinimalViewPortConfig } from '@synchro-charts/core'; +import { v4 } from 'uuid'; + +type ViewportListener = (viewport: MinimalViewPortConfig) => void; + +let listenerMap: { [group: string]: { [id: string]: ViewportListener } } = {}; +let viewportMap: { [group: string]: MinimalViewPortConfig } = {}; +/** + * Publicly exposed manager of viewport groups. Allows components, both internally to IoT App Kit, + * and external components / code to broadcast updates to viewports within a group. + * + * Utilized to allow widgets to provide a synchronized view into data - an example can be + * found at https://synchrocharts.com/#/Features/Synchronization + */ +export const viewportManager = { + /** + * Resets all state related to viewport groups. + */ + reset: () => { + listenerMap = {}; + viewportMap = {}; + }, + /** + * Subscribe to viewport group + * + * @param viewportGroup - group to subscribe to + * @param viewportListener - listener for viewport group updates. Called every time an update to the group is called. Not called upon initial subscription + */ + subscribe: ( + viewportGroup: string, + viewportListener: (viewport: MinimalViewPortConfig) => void + ): { + unsubscribe: () => void; + viewport: MinimalViewPortConfig | null; + } => { + const id = v4(); + if (listenerMap[viewportGroup] == null) { + listenerMap[viewportGroup] = {}; + } + listenerMap[viewportGroup][id] = viewportListener; + + return { + // Current viewport for the group + viewport: viewportMap[viewportGroup], + // Leave viewport group, prevents listener from being called in the future + unsubscribe: () => { + delete listenerMap[viewportGroup][id]; + }, + }; + }, + update: (viewportGroup: string, viewport: MinimalViewPortConfig): void => { + viewportMap[viewportGroup] = viewport; + // broadcast update to all listeners within the group + Object.keys(listenerMap[viewportGroup] || {}).forEach((id) => listenerMap[viewportGroup][id](viewport)); + }, +};