From caa72b6dd7bdc81549c17ed7c53ea6e4d01b15a3 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 11 May 2022 13:12:42 -0600 Subject: [PATCH 1/2] Simple multi-cast, multi-channel communication foundation layer --- .../javascripts/lib/utils/communication.js | 18 +++ package.json | 1 + spec/frontend/lib/utils/communication_spec.js | 117 ++++++++++++++++++ yarn.lock | 7 ++ 4 files changed, 143 insertions(+) create mode 100644 app/assets/javascripts/lib/utils/communication.js create mode 100644 spec/frontend/lib/utils/communication_spec.js diff --git a/app/assets/javascripts/lib/utils/communication.js b/app/assets/javascripts/lib/utils/communication.js new file mode 100644 index 00000000000000..a750c77edb5345 --- /dev/null +++ b/app/assets/javascripts/lib/utils/communication.js @@ -0,0 +1,18 @@ +import { Subject } from 'rxjs'; + +const channels = {}; + +export function openChannel({ name }) { + let channel = channels[name]; + + if (!channel) { + channel = new Subject(); + channels[name] = channel; + } + + return channel; +} + +export function flushChannel({ name }) { + channels[name] = null; +} diff --git a/package.json b/package.json index 73778428426c4a..34d728b454d933 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "rehype-raw": "^6.1.1", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", + "rxjs": "7.5.5", "scrollparent": "^2.0.1", "select2": "3.5.2-browserify", "smooshpack": "^0.0.62", diff --git a/spec/frontend/lib/utils/communication_spec.js b/spec/frontend/lib/utils/communication_spec.js new file mode 100644 index 00000000000000..f58ea78656aae0 --- /dev/null +++ b/spec/frontend/lib/utils/communication_spec.js @@ -0,0 +1,117 @@ +import { openChannel, flushChannel } from '~/lib/utils/communication'; + +describe('communication', () => { + describe('openChannel', () => { + let channel; + + beforeEach(() => { + channel = openChannel({ name: 'channel' }); + }); + + afterEach(() => { + flushChannel({ name: 'channel' }); + }); + + it("should return a 'nextable' object", () => { + expect(channel.next).toBeInstanceOf(Function); + expect(channel.error).toBeInstanceOf(Function); + expect(channel.complete).toBeInstanceOf(Function); + }); + + it('should return an observable Subject', () => { + expect(channel.subscribe).toBeInstanceOf(Function); + expect(channel.thrownError).toBeNull(); + expect(channel.closed).toBe(false); + expect(channel.observed).toBe(false); + }); + + it('should always return the same channel for a given name', () => { + expect(channel).toBe(openChannel({ name: 'channel' })); + }); + }); + + describe('flushChannel', () => { + it('should not return the same channel if the channel is flushed between opens', () => { + const channel = openChannel({ name: 'channel' }); + + flushChannel({ name: 'channel' }); + + expect(channel).not.toBe(openChannel({ name: 'channel' })); + }); + }); + + describe('channel', () => { + let channel; + + beforeEach(() => { + channel = openChannel({ name: 'channel' }); + }); + + afterEach(() => { + flushChannel({ name: 'channel' }); + }); + + it('should send events to observers', () => { + const events = []; + + channel.subscribe((event) => { + events.push(event.message); + }); + + channel.next({ message: 'sent' }); + + expect(events).toEqual(['sent']); + }); + + it('should pass on a completion to observers', () => { + channel.subscribe({ + complete: () => expect(true).toBe(true), // We just want to test that this runs + }); + + channel.complete(); + }); + + it('should not send events after completing', () => { + const events = []; + + channel.subscribe({ next: (event) => events.push(event) }); + channel.complete(); + + channel.next({ name: 'sent' }); + + expect(events).toEqual([]); + }); + + it('should pass on an error to observers', async () => { + let caught; + + channel.subscribe({ + error: (e) => { + caught = e; + }, + }); + + channel.error('broke'); + + expect(caught).toBe('broke'); + }); + + it('should not send events after erroring', () => { + const events = []; + + channel.subscribe({ + next: (e) => events.push(e), + error: () => { + /* swallow errors */ + }, + }); + + channel.next('a'); + channel.next('b'); + channel.error('error'); + channel.next('c'); + + expect(events).toStrictEqual(['a', 'b']); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 236523c3625171..bd9ce254c7ea93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10757,6 +10757,13 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= +rxjs@7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + sade@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" -- GitLab From a0bb152e74551d31c353d2fdb9e762f5a1b4d876 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Mon, 16 May 2022 18:36:45 -0600 Subject: [PATCH 2/2] Add JSDoc blocks for the communication harness base implementation --- .../javascripts/lib/utils/communication.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/assets/javascripts/lib/utils/communication.js b/app/assets/javascripts/lib/utils/communication.js index a750c77edb5345..3af2afe5a101cf 100644 --- a/app/assets/javascripts/lib/utils/communication.js +++ b/app/assets/javascripts/lib/utils/communication.js @@ -1,7 +1,22 @@ +/** + * @module communication + */ + +/** + * [RxJS Observable Subject]{@link https://rxjs.dev/api/index/class/Subject} + * @typedef {Object} RxSubject + */ + import { Subject } from 'rxjs'; const channels = {}; +/** + * Open a channel on the communication harness + * @param {Object} [options] + * @param {String} [options.name] - The name of the channel to retrieve or create + * @returns {RxSubject} The requested channel, if it exists, or else a new channel + */ export function openChannel({ name }) { let channel = channels[name]; @@ -13,6 +28,11 @@ export function openChannel({ name }) { return channel; } +/** + * Remove references to a channel from the communication harness + * @param {Object} [options] + * @param {String} [options.name] - The name of the channel to flush (forget/dereference) + */ export function flushChannel({ name }) { channels[name] = null; } -- GitLab