diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index e6e83f7..3f8ff75 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -61,6 +61,15 @@ export interface IMessage { * Represents a messagebroker and provides access to the core features which includes publishing/subscribing to messages and RSVP. */ export interface IMessageBroker { + /** + * A reference to the parent scope if this is not the root node in the tree of scopes. If this is the root, it's undefined. + */ + readonly parent?: IMessageBroker; + /** + * A list of all child scopes that have been created on this instance of the broker. + */ + readonly scopes: IMessageBroker[]; + /** * Creates a new channel with the provided channelName. An optional config object can be passed that specifies how many messages to cache. * No caching is set by default @@ -96,6 +105,14 @@ export interface IMessageBroker { * This RSVP function is used by responders and is analogous to the 'Get' function. Responders when invoked must return the required response value type. */ rsvp>(channelName: K, handler: RSVPHandler): IResponderRef; + + /** + * Creates a new scope with the given scopeName with this instance of the MessageBroker as its parent. + * If a scope with this name already exists, it returns that instance instead of creating a new one. + * @param scopeName The name to use for the scope to create + * @returns An instance of the messagebroker that matches the scopeName provided + */ + createScope(scopeName: string): IMessageBroker; } /** diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index f1d723c..cafce5d 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -1,4 +1,4 @@ -import { get, Injectable } from '@morgan-stanley/needle'; +import { get, getRootInjector, Injectable } from '@morgan-stanley/needle'; import { defer, Observable, Subject, Subscription } from 'rxjs'; import { filter, shareReplay } from 'rxjs/operators'; import { v4 as uuid } from 'uuid'; @@ -29,6 +29,8 @@ export function messagebroker(): IMessageBroker { return instance; } +const rootInjector = getRootInjector(); + /** * Represents a messagebroker. Using the 'new' operator is discouraged, instead use the messagebroker() function or dependency injection. */ @@ -36,8 +38,9 @@ export function messagebroker(): IMessageBroker { export class MessageBroker implements IMessageBroker { private channelLookup: ChannelModelLookup = {}; private messagePublisher = new Subject>(); + private _scopes: IMessageBroker[] = []; - constructor(private rsvpMediator: RSVPMediator) {} + constructor(private rsvpMediator: RSVPMediator, private _parent?: IMessageBroker) {} /** * Creates a new channel with the provided channelName. An optional config object can be passed that specifies how many messages to cache. @@ -99,6 +102,21 @@ export class MessageBroker implements IMessageBroker { delete this.channelLookup[channelName]; } + /** + * Creates a new scope with the given scopeName with this instance of the MessageBroker as its parent. + * If a scope with this name already exists, it returns that instance instead of creating a new one. + * @param scopeName The name to use for the scope to create + * @returns An instance of the messagebroker that matches the scopeName provided + */ + public createScope(scopeName: string): IMessageBroker { + const scope = rootInjector.createScope(scopeName); + scope.registerInstance(MessageBroker, new MessageBroker(new RSVPMediator(), this)); + + const instance = scope.get(MessageBroker); + this._scopes.push(instance); + return instance; + } + /** * Return a deferred observable as the channel config may have been updated before the subscription * @param channelName name of channel to subscribe to @@ -143,6 +161,7 @@ export class MessageBroker implements IMessageBroker { } const publishFunction = (data?: T[K], type?: string): void => { + this._scopes.forEach((scope) => scope.create(channelName).publish(data), type); this.messagePublisher.next(this.createMessage(channelName, data, type)); }; @@ -180,4 +199,12 @@ export class MessageBroker implements IMessageBroker { ): channel is RequiredPick, 'config' | 'subscription'> { return channel != null && channel.subscription != null; } + + public get parent(): IMessageBroker | undefined { + return this._parent; + } + + public get scopes(): IMessageBroker[] { + return this._scopes; + } } diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index 8ed9988..3bb2e2f 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -368,6 +368,79 @@ describe('MessageBroker', () => { }); }); + describe('Scopes', () => { + it('should return a new messagebroker instance when creating a new scope', () => { + const instance = getInstance(); + const scope = instance.createScope('scope1'); + + expect(scope).not.toEqual(instance); + }); + + it('should return same scope if same name is used', () => { + const instance = getInstance(); + const scope = instance.createScope('scope1'); + const sameScope = instance.createScope('scope1'); + + expect(scope).toEqual(sameScope); + }); + + it('should return itself when getting the parent of its child', () => { + const instance = getInstance(); + const scope = instance.createScope('scope1'); + + expect(scope.parent).toEqual(instance); + }); + + it('should return a list of children scopes via scopes property', () => { + const instance = getInstance(); + const scope1 = instance.createScope('scope1'); + const scope2 = instance.createScope('scope2'); + const scope3 = instance.createScope('scope3'); + + expect(instance.scopes).toEqual([scope1, scope2, scope3]); + }); + + it('should publish messages from parent to children', () => { + const parentMessages: Array> = []; + const childMessages: Array> = []; + const parent = getInstance(); + const child = parent.createScope('scope1'); + + parent.get('channel').subscribe((message) => parentMessages.push(message)); + child.get('channel').subscribe((message) => childMessages.push(message)); + + parent.create('channel').publish('both should get this'); + child.create('channel').publish('only the child should get this'); + + expect(parentMessages.length).toEqual(1); + verifyMessage(parentMessages[0], 'both should get this'); + + expect(childMessages.length).toEqual(2); + verifyMessage(childMessages[0], 'both should get this'); + verifyMessage(childMessages[1], 'only the child should get this'); + }); + + it('should not publish messages to "sibling" scopes', () => { + const brotherMessages: Array> = []; + const sisterMessages: Array> = []; + const parent = getInstance(); + const brother = parent.createScope('scope1'); + const sister = parent.createScope('scope2'); + + brother.get('channel').subscribe((message) => brotherMessages.push(message)); + sister.get('channel').subscribe((message) => sisterMessages.push(message)); + + brother.create('channel').publish('brother should get this'); + sister.create('channel').publish('sister should get this'); + + expect(brotherMessages.length).toEqual(1); + verifyMessage(brotherMessages[0], 'brother should get this'); + + expect(sisterMessages.length).toEqual(1); + verifyMessage(sisterMessages[0], 'sister should get this'); + }); + }); + function verifyMessage(message: IMessage, expectedData: T, expectedType?: string) { expect(message).toBeDefined(); expect(message.data).toEqual(expectedData);