From cbb6b905047aab528f0de5b85756934ab513e5b7 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Tue, 3 Sep 2024 12:42:07 +0100 Subject: [PATCH 01/17] preliminary scopes implementation --- main/contracts/contracts.ts | 17 ++++++++ main/core/messagebroker.ts | 31 +++++++++++++- spec/core/messagebroker.spec.ts | 73 +++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) 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); From 67c01ede337dda3a193027afecbfbb8ce17a3ffc Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 18 Oct 2024 15:06:21 +0100 Subject: [PATCH 02/17] added an initial doc explaining scopes --- site/content/documentation/1.0.0/scopes.mdx | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 site/content/documentation/1.0.0/scopes.mdx diff --git a/site/content/documentation/1.0.0/scopes.mdx b/site/content/documentation/1.0.0/scopes.mdx new file mode 100644 index 0000000..a5dbc6e --- /dev/null +++ b/site/content/documentation/1.0.0/scopes.mdx @@ -0,0 +1,52 @@ +--- +order: 5 +title: Scopes +--- + +Message scoping is a mechanism for restricting which subscirbers will receive a given message. + +```typescript +const messagebroker: IMessageBroker = messagebroker(); +const child: IMessageBroker = messagebroker.createScope('my-scope'); +const sibling: IMessageBroker = messagebroker.createScope('my-second-scope'); +``` + +## Publish + +### Parent Publish + +Any message that is published to a broker is also published to the children scopes of that broker. + +```typescript +child.subscribe(message => console.log('child received')); +messagebroker.subscribe(message => console.log('parent received')); +messagebroker.publish({}); + +// console: child received +// console: parent received +``` + +### Child Publish + +Messages are not received by the parent scope of a given broker. + +```typescript +child.subscribe(message => console.log('child received')); +messagebroker.subscribe(message => console.log('parent received')); +child.publish({}); + +// console: child received +``` + +### Sibling Publish + +Messages are also not published to "sibling" scopes, where the brokers share a parent. + +```typescript +child.subscribe(message => console.log('child received')); +sibling.subscribe(message => console.log('sibling received')); +messagebroker.subscribe(message => console.log('parent received')); +sibling.publish({}); + +// console: sibling received +``` From e376e31abdc5011eed73f8eaa10551190805da13 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 18 Oct 2024 15:06:55 +0100 Subject: [PATCH 03/17] refactored scopes, added more tests and scope disposal --- main/core/messagebroker.ts | 27 ++++++++++++++--------- spec/core/messagebroker.spec.ts | 38 ++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index cafce5d..37034a1 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -1,4 +1,4 @@ -import { get, getRootInjector, Injectable } from '@morgan-stanley/needle'; +import { get, 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,8 +29,6 @@ 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. */ @@ -38,7 +36,10 @@ const rootInjector = getRootInjector(); export class MessageBroker implements IMessageBroker { private channelLookup: ChannelModelLookup = {}; private messagePublisher = new Subject>(); - private _scopes: IMessageBroker[] = []; + private _scopes: { + name: string; + instance: IMessageBroker; + }[] = []; constructor(private rsvpMediator: RSVPMediator, private _parent?: IMessageBroker) {} @@ -95,6 +96,7 @@ export class MessageBroker implements IMessageBroker { * @param channelName Name of the messagebroker channel */ public dispose(channelName: K): void { + this._scopes.forEach((scope) => scope.instance.dispose(channelName)); const channel = this.channelLookup[channelName]; if (this.isChannelConfiguredWithCaching(channel)) { channel.subscription.unsubscribe(); @@ -109,11 +111,16 @@ export class MessageBroker implements IMessageBroker { * @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 existingScope = this._scopes.find((scope) => scope.name === scopeName); + if (existingScope) { + return existingScope.instance; + } - const instance = scope.get(MessageBroker); - this._scopes.push(instance); + const instance = new MessageBroker(this.rsvpMediator, this); + this._scopes.push({ + name: scopeName, + instance, + }); return instance; } @@ -161,7 +168,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._scopes.forEach((scope) => scope.instance.create(channelName).publish(data), type); this.messagePublisher.next(this.createMessage(channelName, data, type)); }; @@ -205,6 +212,6 @@ export class MessageBroker implements IMessageBroker { } public get scopes(): IMessageBroker[] { - return this._scopes; + return this._scopes.map((scope) => scope.instance); } } diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index 3bb2e2f..27005fb 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -147,6 +147,18 @@ describe('MessageBroker', () => { expect(postDisposeNextFunction).not.toBe(channel); }); + it('should dipose of child scope channels as well', () => { + const instance = getInstance(); + const child = instance.createScope('child'); + const channel = child.create('yourChannel'); + + instance.dispose('yourChannel'); // dispose of the channel in the PARENT + + const postDisposeNextFunction = child.create('yourChannel'); + + expect(postDisposeNextFunction).not.toBe(channel); + }); + it('should allow publishing of channel message without data', () => { const instance = getInstance(); const channel = instance.create('yourChannel'); @@ -373,7 +385,7 @@ describe('MessageBroker', () => { const instance = getInstance(); const scope = instance.createScope('scope1'); - expect(scope).not.toEqual(instance); + expect(scope).not.toBe(instance); }); it('should return same scope if same name is used', () => { @@ -381,14 +393,14 @@ describe('MessageBroker', () => { const scope = instance.createScope('scope1'); const sameScope = instance.createScope('scope1'); - expect(scope).toEqual(sameScope); + expect(scope).toBe(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); + expect(scope.parent).toBe(instance); }); it('should return a list of children scopes via scopes property', () => { @@ -439,6 +451,26 @@ describe('MessageBroker', () => { expect(sisterMessages.length).toEqual(1); verifyMessage(sisterMessages[0], 'sister should get this'); }); + + it('should not publish messages to scopes with the same name', () => { + const scope1Message: Array> = []; + const scope2Message: Array> = []; + const root = getInstance(); + const testScope = root.createScope('duplicated-scope'); + const duplicateNameScope = root.createScope('middle').createScope('duplicated-scope'); + + testScope.get('channel').subscribe((message) => scope1Message.push(message)); + duplicateNameScope.get('channel').subscribe((message) => scope2Message.push(message)); + + testScope.create('channel').publish('first message'); + duplicateNameScope.create('channel').publish('second message'); + + expect(scope1Message.length).toEqual(1); + verifyMessage(scope1Message[0], 'first message'); + + expect(scope2Message.length).toEqual(1); + verifyMessage(scope2Message[0], 'second message'); + }); }); function verifyMessage(message: IMessage, expectedData: T, expectedType?: string) { From 75fe8727c64bf315e7f281b8d303060a01d5dc18 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 18 Oct 2024 16:04:23 +0100 Subject: [PATCH 04/17] improved documentation --- site/content/documentation/1.0.0/scopes.mdx | 97 +++++++++++++++------ 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/site/content/documentation/1.0.0/scopes.mdx b/site/content/documentation/1.0.0/scopes.mdx index a5dbc6e..81b7d7c 100644 --- a/site/content/documentation/1.0.0/scopes.mdx +++ b/site/content/documentation/1.0.0/scopes.mdx @@ -3,50 +3,97 @@ order: 5 title: Scopes --- -Message scoping is a mechanism for restricting which subscirbers will receive a given message. +Message scoping is a mechanism for restricting which subscribers will receive a given message. +You can create a new scope on a messagebroker by calling `createScope`. ```typescript -const messagebroker: IMessageBroker = messagebroker(); -const child: IMessageBroker = messagebroker.createScope('my-scope'); -const sibling: IMessageBroker = messagebroker.createScope('my-second-scope'); +const parent: IMessageBroker + = messagebroker(); + +const child: IMessageBroker + = parent.createScope('my-scope'); ``` -## Publish +A scope is just another instance of an `IMessageBroker` on which you can perform all of the same operations that you'd expect on the base messagebroker. +The main thing to note about this feature is how messages are shared across scopes. -### Parent Publish +### Scope Hierarchies -Any message that is published to a broker is also published to the children scopes of that broker. +Any message that is published to a broker is also published down through the hierarchy of children scopes belonging that broker. ```typescript -child.subscribe(message => console.log('child received')); -messagebroker.subscribe(message => console.log('parent received')); -messagebroker.publish({}); +parent.get('x').subscribe(message => console.log('parent received')); +child.get('x').subscribe(message => console.log('child received')); + +parent.create('x').publish({}); -// console: child received -// console: parent received +// expect: child received +// expect: parent received ``` -### Child Publish +However messages are not sent **up** the hierarchy to the parent of that broker. + +```typescript +parent.get('x').subscribe(message => console.log('parent received')); +child.get('x').subscribe(message => console.log('child received')); + +child.create('x').publish({}); -Messages are not received by the parent scope of a given broker. +// expect: child received +``` + +Messages are also not published to "sibling" scopes, where the brokers share a parent. ```typescript -child.subscribe(message => console.log('child received')); -messagebroker.subscribe(message => console.log('parent received')); -child.publish({}); +const sibling: IMessageBroker + = parent.createScope('sibling-scope'); + +parent.get('x').subscribe(message => console.log('parent received')); +child.get('x').subscribe(message => console.log('child received')); +sibling.get('x').subscribe(message => console.log('sibling received')); -// console: child received +sibling.create('x').publish({}); + +// expect: sibling received ``` -### Sibling Publish +### Scope Depth -Messages are also not published to "sibling" scopes, where the brokers share a parent. +Scope hierarchies can be arbitrarily deep, and messages will make their way all the way down to the bottom. ```typescript -child.subscribe(message => console.log('child received')); -sibling.subscribe(message => console.log('sibling received')); -messagebroker.subscribe(message => console.log('parent received')); -sibling.publish({}); +const distantChild = parent + .createScope('scope1') + .createScope('scope2') + ... + .createScope('scopeX'); -// console: sibling received +distantChild.get('x').subscribe(message => console.log('child received')); + +parent.create('x').publish({}); + +// expect: child received ``` + +### Naming + +Scopes under the same parent cannot have the same name. +An attempt to create a scope with a name that already exists on a broker will just return the original scope. + +If they are different parts of the hierarchy (i.e. don't share a parent), then you can have multiple scopes with the same name. + +### Disposal + +Disposing of a channel in a broker will also dispose of that channel in its children scopes. + +```typescript +const parentChannel = parent.create('x'); +const childChannel = child.create('x'); +parentChannel.subscribe(message => console.log('parent received')); +childChannel.subscribe(message => console.log('child received')); + +parentChannel.dispose('x'); // cleans up child subscription too +parentChannel.publish({}); + +// expect: nothing... +``` \ No newline at end of file From 0389a8b4b8c29c979a6c86b19e106d1c75212bc5 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Wed, 13 Nov 2024 16:19:32 +0000 Subject: [PATCH 05/17] renamed scopes to children --- main/contracts/contracts.ts | 2 +- main/core/messagebroker.ts | 14 +++++++------- spec/core/messagebroker.spec.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index 3f8ff75..485a580 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -68,7 +68,7 @@ export interface IMessageBroker { /** * A list of all child scopes that have been created on this instance of the broker. */ - readonly scopes: IMessageBroker[]; + readonly children: IMessageBroker[]; /** * Creates a new channel with the provided channelName. An optional config object can be passed that specifies how many messages to cache. diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index 37034a1..3550c1f 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -36,7 +36,7 @@ export function messagebroker(): IMessageBroker { export class MessageBroker implements IMessageBroker { private channelLookup: ChannelModelLookup = {}; private messagePublisher = new Subject>(); - private _scopes: { + private _children: { name: string; instance: IMessageBroker; }[] = []; @@ -96,7 +96,7 @@ export class MessageBroker implements IMessageBroker { * @param channelName Name of the messagebroker channel */ public dispose(channelName: K): void { - this._scopes.forEach((scope) => scope.instance.dispose(channelName)); + this._children.forEach((scope) => scope.instance.dispose(channelName)); const channel = this.channelLookup[channelName]; if (this.isChannelConfiguredWithCaching(channel)) { channel.subscription.unsubscribe(); @@ -111,13 +111,13 @@ export class MessageBroker implements IMessageBroker { * @returns An instance of the messagebroker that matches the scopeName provided */ public createScope(scopeName: string): IMessageBroker { - const existingScope = this._scopes.find((scope) => scope.name === scopeName); + const existingScope = this._children.find((scope) => scope.name === scopeName); if (existingScope) { return existingScope.instance; } const instance = new MessageBroker(this.rsvpMediator, this); - this._scopes.push({ + this._children.push({ name: scopeName, instance, }); @@ -168,7 +168,7 @@ export class MessageBroker implements IMessageBroker { } const publishFunction = (data?: T[K], type?: string): void => { - this._scopes.forEach((scope) => scope.instance.create(channelName).publish(data), type); + this._children.forEach((scope) => scope.instance.create(channelName).publish(data), type); this.messagePublisher.next(this.createMessage(channelName, data, type)); }; @@ -211,7 +211,7 @@ export class MessageBroker implements IMessageBroker { return this._parent; } - public get scopes(): IMessageBroker[] { - return this._scopes.map((scope) => scope.instance); + public get children(): IMessageBroker[] { + return this._children.map((scope) => scope.instance); } } diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index 27005fb..ab4c13e 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -409,7 +409,7 @@ describe('MessageBroker', () => { const scope2 = instance.createScope('scope2'); const scope3 = instance.createScope('scope3'); - expect(instance.scopes).toEqual([scope1, scope2, scope3]); + expect(instance.children).toEqual([scope1, scope2, scope3]); }); it('should publish messages from parent to children', () => { From 22422cd8123cd2e1143d0551794904f74845db93 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Wed, 13 Nov 2024 16:22:18 +0000 Subject: [PATCH 06/17] added an isRoot method to the messagebroker --- main/contracts/contracts.ts | 7 +++++++ main/core/messagebroker.ts | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index 485a580..cca4f92 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -113,6 +113,13 @@ export interface IMessageBroker { * @returns An instance of the messagebroker that matches the scopeName provided */ createScope(scopeName: string): IMessageBroker; + + /** + * Returns true if this is root node of the tree of MessageBrokers. + * The root MessageBroker will not have a parent MessageBroker. + * @returns A boolean indicating whether this is the root or not + */ + isRoot(): boolean; } /** diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index 3550c1f..2c111ca 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -207,6 +207,10 @@ export class MessageBroker implements IMessageBroker { return channel != null && channel.subscription != null; } + public isRoot(): boolean { + return this._parent === undefined; + } + public get parent(): IMessageBroker | undefined { return this._parent; } From 5a367802234008468243ce1de8beed7e3615b218 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 6 Dec 2024 11:11:30 +0000 Subject: [PATCH 07/17] improved destruction of scopes --- main/contracts/contracts.ts | 8 ++- main/core/messagebroker.ts | 48 +++++++++---- site/content/documentation/1.0.0/scopes.mdx | 16 ++--- spec/core/messagebroker.spec.ts | 77 +++++++++++++++++++++ tsconfig.json | 3 +- 5 files changed, 128 insertions(+), 24 deletions(-) diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index cca4f92..06f05c6 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -61,6 +61,10 @@ 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 unique identifier for this instance of the MessageBroker. This is useful for identifying an instance within a tree of scopes. + */ + readonly name: string; /** * 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. */ @@ -68,7 +72,7 @@ export interface IMessageBroker { /** * A list of all child scopes that have been created on this instance of the broker. */ - readonly children: IMessageBroker[]; + children: IMessageBroker[]; /** * Creates a new channel with the provided channelName. An optional config object can be passed that specifies how many messages to cache. @@ -114,6 +118,8 @@ export interface IMessageBroker { */ createScope(scopeName: string): IMessageBroker; + destroy(): void; + /** * Returns true if this is root node of the tree of MessageBrokers. * The root MessageBroker will not have a parent MessageBroker. diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index 2c111ca..b9f949d 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -36,12 +36,13 @@ export function messagebroker(): IMessageBroker { export class MessageBroker implements IMessageBroker { private channelLookup: ChannelModelLookup = {}; private messagePublisher = new Subject>(); - private _children: { - name: string; - instance: IMessageBroker; - }[] = []; + private _children: IMessageBroker[] = []; - constructor(private rsvpMediator: RSVPMediator, private _parent?: IMessageBroker) {} + constructor( + private rsvpMediator: RSVPMediator, + private _parent?: IMessageBroker, + private _name: string = 'root', + ) {} /** * Creates a new channel with the provided channelName. An optional config object can be passed that specifies how many messages to cache. @@ -96,7 +97,7 @@ export class MessageBroker implements IMessageBroker { * @param channelName Name of the messagebroker channel */ public dispose(channelName: K): void { - this._children.forEach((scope) => scope.instance.dispose(channelName)); + this._children.forEach((scope) => scope.dispose(channelName)); const channel = this.channelLookup[channelName]; if (this.isChannelConfiguredWithCaching(channel)) { channel.subscription.unsubscribe(); @@ -111,19 +112,30 @@ export class MessageBroker implements IMessageBroker { * @returns An instance of the messagebroker that matches the scopeName provided */ public createScope(scopeName: string): IMessageBroker { + if (scopeName === 'root') return this; + const existingScope = this._children.find((scope) => scope.name === scopeName); if (existingScope) { - return existingScope.instance; + return existingScope; } - const instance = new MessageBroker(this.rsvpMediator, this); - this._children.push({ - name: scopeName, - instance, - }); + const instance = new MessageBroker(this.rsvpMediator, this, scopeName); + this._children.push(instance); return instance; } + public destroy(): void { + this._children.forEach((childScope) => childScope.destroy()); + + type Channels = (keyof typeof this.channelLookup)[]; + (Object.keys(this.channelLookup) as Channels).forEach((channelName) => this.dispose(channelName)); + + if (this._parent) { + this._parent.children = this._parent.children.filter((child) => child !== this); // remove itself from the parent + this._parent = undefined; + } + } + /** * Return a deferred observable as the channel config may have been updated before the subscription * @param channelName name of channel to subscribe to @@ -168,7 +180,7 @@ export class MessageBroker implements IMessageBroker { } const publishFunction = (data?: T[K], type?: string): void => { - this._children.forEach((scope) => scope.instance.create(channelName).publish(data), type); + this._children.forEach((scope) => scope.create(channelName).publish(data), type); // propagate messages to children this.messagePublisher.next(this.createMessage(channelName, data, type)); }; @@ -216,6 +228,14 @@ export class MessageBroker implements IMessageBroker { } public get children(): IMessageBroker[] { - return this._children.map((scope) => scope.instance); + return this._children; + } + + protected set children(children: IMessageBroker[]) { + this._children = children; + } + + public get name(): string { + return this._name; } } diff --git a/site/content/documentation/1.0.0/scopes.mdx b/site/content/documentation/1.0.0/scopes.mdx index 81b7d7c..c3fd0d5 100644 --- a/site/content/documentation/1.0.0/scopes.mdx +++ b/site/content/documentation/1.0.0/scopes.mdx @@ -12,6 +12,8 @@ const parent: IMessageBroker const child: IMessageBroker = parent.createScope('my-scope'); + +parent.children.contains(child) // expect: true ``` A scope is just another instance of an `IMessageBroker` on which you can perform all of the same operations that you'd expect on the base messagebroker. @@ -86,14 +88,12 @@ If they are different parts of the hierarchy (i.e. don't share a parent), then y Disposing of a channel in a broker will also dispose of that channel in its children scopes. -```typescript -const parentChannel = parent.create('x'); -const childChannel = child.create('x'); -parentChannel.subscribe(message => console.log('parent received')); -childChannel.subscribe(message => console.log('child received')); +### Destroy -parentChannel.dispose('x'); // cleans up child subscription too -parentChannel.publish({}); +A MessageBroker instance can be destroyed. +Destroying a MessageBroker will first destroy all of its children, it will dispose of all its channels, and finally remove itself from its parent. -// expect: nothing... +```typescript +child.destroy(); +parent.children.contains(child); // expect: false ``` \ No newline at end of file diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index ab4c13e..d77bc47 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -471,6 +471,83 @@ describe('MessageBroker', () => { expect(scope2Message.length).toEqual(1); verifyMessage(scope2Message[0], 'second message'); }); + + describe('Destroy', () => { + it('should dispose of all subscriptions on that instance and its child', () => { + const instance = getInstance(); + const instanceChannel = instance.create('yourChannel'); + const child = instance.createScope('child'); + const childChannel = instance.create('yourChannel'); + + instance.destroy(); // destroy the PARENT + + const postDisposeInstanceChannel = instance.create('yourChannel'); + const postDisposeChildChannel = child.create('yourChannel'); + + expect(postDisposeInstanceChannel).not.toBe(instanceChannel); + expect(postDisposeChildChannel).not.toBe(childChannel); + }); + + it('should remove itself from its parents children', () => { + const parent = getInstance(); + const child = parent.createScope('child'); + + expect(parent.children).toContain(child); + child.destroy(); + expect(parent.children).not.toContain(child); + }); + + it('should remove its parent', () => { + const parent = getInstance(); + const child = parent.createScope('child'); + + child.destroy(); + expect(child.parent).toBeUndefined(); + }); + + it('should prevent message propagation from happening', () => { + const childMessages: Array> = []; + const parentMessages: Array> = []; + const parent = getInstance(); + const child = parent.createScope('child'); + + child.get('channel').subscribe((message) => childMessages.push(message)); + parent.get('channel').subscribe((message) => parentMessages.push(message)); + + child.destroy(); + + parent.create('channel').publish('message'); + + expect(childMessages.length).toEqual(0); + + expect(parentMessages.length).toEqual(1); + verifyMessage(parentMessages[0], 'message'); + }); + + it('should destroy all child scopes as well', () => { + const parent = getInstance(); + const child = parent.createScope('child'); + + const parentChannel = parent.create('channel', { replayCacheSize: 2 }); + const childChannel = child.create('channel', { replayCacheSize: 2 }); + + parentChannel.publish('message one'); + parentChannel.publish('message two'); + + parent.destroy(); // this should cancel the existing caching subscriptions + + const childMessages: Array> = []; + const parentMessages: Array> = []; + parentChannel.stream.subscribe((message) => parentMessages.push(message)); + childChannel.stream.subscribe((message) => childMessages.push(message)); + + parentChannel.publish('message three'); + + expect(childMessages.length).toEqual(0); + expect(parentMessages.length).toEqual(1); + verifyMessage(parentMessages[0], 'message three'); + }); + }); }); function verifyMessage(message: IMessage, expectedData: T, expectedType?: string) { diff --git a/tsconfig.json b/tsconfig.json index fa00f2b..089fe2c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { - "target": "ES5", + "target": "ES2017", "module": "commonjs", "lib": [ "ES5", "ES6", "DOM", + "ES2017" ], "declaration": true, "sourceMap": true, From 3e5b2bc0a33284c151b8180dd62235774da9da80 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 6 Dec 2024 11:15:27 +0000 Subject: [PATCH 08/17] added tests around name field --- spec/core/messagebroker.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index d77bc47..5dc66c7 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -388,6 +388,17 @@ describe('MessageBroker', () => { expect(scope).not.toBe(instance); }); + it('should give the name root to the initial messagebroker instance', () => { + const instance = getInstance(); + expect(instance.name).toBe('root'); + }); + + it('should name child scopes correctly', () => { + const instance = getInstance(); + const child = instance.createScope('myChild'); + expect(child.name).toBe('myChild'); + }); + it('should return same scope if same name is used', () => { const instance = getInstance(); const scope = instance.createScope('scope1'); From 5622030310c64c99588cf91bb834052bcf3fea15 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 6 Dec 2024 11:17:36 +0000 Subject: [PATCH 09/17] added docs to destroy function --- main/contracts/contracts.ts | 4 ++++ main/core/messagebroker.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index 06f05c6..ee4e2b2 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -118,6 +118,10 @@ export interface IMessageBroker { */ createScope(scopeName: string): IMessageBroker; + /* + * Destroys all children scopes, disposes of all message channels on + * this instance and removes itself from its parents children. + */ destroy(): void; /** diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index b9f949d..05633f1 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -124,6 +124,10 @@ export class MessageBroker implements IMessageBroker { return instance; } + /* + * Destroys all children scopes, disposes of all message channels on + * this instance and removes itself from its parents children. + */ public destroy(): void { this._children.forEach((childScope) => childScope.destroy()); From 795a10ad6ddd8855d6edebebf4c056d4e04c1b0e Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 6 Dec 2024 11:18:42 +0000 Subject: [PATCH 10/17] removed confusing logic around root node --- main/core/messagebroker.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index 05633f1..a9ae5e4 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -112,8 +112,6 @@ export class MessageBroker implements IMessageBroker { * @returns An instance of the messagebroker that matches the scopeName provided */ public createScope(scopeName: string): IMessageBroker { - if (scopeName === 'root') return this; - const existingScope = this._children.find((scope) => scope.name === scopeName); if (existingScope) { return existingScope; From 12a6b90f5e02b674f203b687a8011d7e4f935f02 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 6 Dec 2024 13:23:37 +0000 Subject: [PATCH 11/17] minor version bump in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 950fbe9..ec94e98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@morgan-stanley/message-broker", - "version": "1.0.0", + "version": "1.1.0", "description": "Framework agnostic messagebroker for decoupled communication.", "main": "dist/main/index.js", "types": "dist/main/index.d.ts", From fe0edebe98dde278a37ab58a0141a4599bc407c4 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 14 Feb 2025 14:54:59 +0000 Subject: [PATCH 12/17] stopped exposing parent and children to user and fixed propogation logic --- main/contracts/contracts.ts | 15 +--- main/core/messagebroker.ts | 49 ++++--------- spec/core/messagebroker.spec.ts | 121 ++++++++------------------------ 3 files changed, 44 insertions(+), 141 deletions(-) diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index ee4e2b2..f0dab05 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -61,19 +61,6 @@ 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 unique identifier for this instance of the MessageBroker. This is useful for identifying an instance within a tree of scopes. - */ - readonly name: string; - /** - * 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. - */ - children: 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 @@ -116,7 +103,7 @@ export interface IMessageBroker { * @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; + createScope(scopeName: string): IMessageBroker; /* * Destroys all children scopes, disposes of all message channels on diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index a9ae5e4..945422a 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -33,16 +33,11 @@ export function messagebroker(): IMessageBroker { * Represents a messagebroker. Using the 'new' operator is discouraged, instead use the messagebroker() function or dependency injection. */ @Injectable() -export class MessageBroker implements IMessageBroker { +export class MessageBroker implements IMessageBroker { private channelLookup: ChannelModelLookup = {}; private messagePublisher = new Subject>(); - private _children: IMessageBroker[] = []; - constructor( - private rsvpMediator: RSVPMediator, - private _parent?: IMessageBroker, - private _name: string = 'root', - ) {} + constructor(private rsvpMediator: RSVPMediator, private _parent?: MessageBroker) {} /** * Creates a new channel with the provided channelName. An optional config object can be passed that specifies how many messages to cache. @@ -97,7 +92,6 @@ export class MessageBroker implements IMessageBroker { * @param channelName Name of the messagebroker channel */ public dispose(channelName: K): void { - this._children.forEach((scope) => scope.dispose(channelName)); const channel = this.channelLookup[channelName]; if (this.isChannelConfiguredWithCaching(channel)) { channel.subscription.unsubscribe(); @@ -111,14 +105,8 @@ export class MessageBroker implements IMessageBroker { * @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 existingScope = this._children.find((scope) => scope.name === scopeName); - if (existingScope) { - return existingScope; - } - - const instance = new MessageBroker(this.rsvpMediator, this, scopeName); - this._children.push(instance); + public createScope(): IMessageBroker { + const instance = new MessageBroker(this.rsvpMediator, this as MessageBroker); return instance; } @@ -127,13 +115,10 @@ export class MessageBroker implements IMessageBroker { * this instance and removes itself from its parents children. */ public destroy(): void { - this._children.forEach((childScope) => childScope.destroy()); - type Channels = (keyof typeof this.channelLookup)[]; (Object.keys(this.channelLookup) as Channels).forEach((channelName) => this.dispose(channelName)); if (this._parent) { - this._parent.children = this._parent.children.filter((child) => child !== this); // remove itself from the parent this._parent = undefined; } } @@ -182,8 +167,16 @@ export class MessageBroker implements IMessageBroker { } const publishFunction = (data?: T[K], type?: string): void => { - this._children.forEach((scope) => scope.create(channelName).publish(data), type); // propagate messages to children - this.messagePublisher.next(this.createMessage(channelName, data, type)); + // If there is any registered subscriber for the channel on this broker, then let those handle the message. + // Otherwise, pass it up the chain to the parent to see if they can handle it. + if (this.messagePublisher.observed) { + this.messagePublisher.next(this.createMessage(channelName, data, type)); + } else if (this._parent) { + // It is possible that this channel being published on does NOT exist on the parent. + // In that case, the message will simply be passed up and ignored + // since no one higher up the chain will be able to create a subscriber for this channel. + this._parent.create(channelName as any).publish(data); + } }; // Stream should return a deferred observable @@ -225,19 +218,7 @@ export class MessageBroker implements IMessageBroker { return this._parent === undefined; } - public get parent(): IMessageBroker | undefined { + protected get parent(): MessageBroker | undefined { return this._parent; } - - public get children(): IMessageBroker[] { - return this._children; - } - - protected set children(children: IMessageBroker[]) { - this._children = children; - } - - public get name(): string { - return this._name; - } } diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index 5dc66c7..0140214 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -24,7 +24,7 @@ describe('MessageBroker', () => { mockRSVPMediator = Mock.create>().setup(setupFunction('rsvp')); }); - function getInstance(): MessageBroker { + function getInstance(): MessageBroker { return new MessageBroker(mockRSVPMediator.mock); } @@ -149,7 +149,7 @@ describe('MessageBroker', () => { it('should dipose of child scope channels as well', () => { const instance = getInstance(); - const child = instance.createScope('child'); + const child = instance.createScope(); const channel = child.create('yourChannel'); instance.dispose('yourChannel'); // dispose of the channel in the PARENT @@ -381,74 +381,50 @@ describe('MessageBroker', () => { }); describe('Scopes', () => { - it('should return a new messagebroker instance when creating a new scope', () => { + it('should return a new messagebroker instance for each new scope', () => { const instance = getInstance(); - const scope = instance.createScope('scope1'); + const scope = instance.createScope(); + const scope2 = instance.createScope(); expect(scope).not.toBe(instance); + expect(scope).not.toBe(scope2); }); - it('should give the name root to the initial messagebroker instance', () => { - const instance = getInstance(); - expect(instance.name).toBe('root'); - }); - - it('should name child scopes correctly', () => { - const instance = getInstance(); - const child = instance.createScope('myChild'); - expect(child.name).toBe('myChild'); - }); - - 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).toBe(sameScope); - }); - - it('should return itself when getting the parent of its child', () => { - const instance = getInstance(); - const scope = instance.createScope('scope1'); - - expect(scope.parent).toBe(instance); - }); + it('should publish messages from child to parent if there is no handler on child', () => { + const parentMessages: Array> = []; + const parent = getInstance(); + const child = parent.createScope(); - 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'); + parent.get('channel').subscribe((message) => parentMessages.push(message)); + child.create('channel').publish('parent should handle this'); - expect(instance.children).toEqual([scope1, scope2, scope3]); + expect(parentMessages.length).toEqual(1); + verifyMessage(parentMessages[0], 'parent should handle this'); }); - it('should publish messages from parent to children', () => { + it('should not publish messages from child to parent if there is a handler on child', () => { const parentMessages: Array> = []; const childMessages: Array> = []; const parent = getInstance(); - const child = parent.createScope('scope1'); + const child = parent.createScope(); 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'); + child.create('channel').publish('child should handle this'); - expect(parentMessages.length).toEqual(1); - verifyMessage(parentMessages[0], 'both should get this'); + expect(parentMessages.length).toEqual(0); - expect(childMessages.length).toEqual(2); - verifyMessage(childMessages[0], 'both should get this'); - verifyMessage(childMessages[1], 'only the child should get this'); + expect(childMessages.length).toEqual(1); + verifyMessage(childMessages[0], 'child should handle 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'); + const brother = parent.createScope(); + const sister = parent.createScope(); brother.get('channel').subscribe((message) => brotherMessages.push(message)); sister.get('channel').subscribe((message) => sisterMessages.push(message)); @@ -463,31 +439,11 @@ describe('MessageBroker', () => { verifyMessage(sisterMessages[0], 'sister should get this'); }); - it('should not publish messages to scopes with the same name', () => { - const scope1Message: Array> = []; - const scope2Message: Array> = []; - const root = getInstance(); - const testScope = root.createScope('duplicated-scope'); - const duplicateNameScope = root.createScope('middle').createScope('duplicated-scope'); - - testScope.get('channel').subscribe((message) => scope1Message.push(message)); - duplicateNameScope.get('channel').subscribe((message) => scope2Message.push(message)); - - testScope.create('channel').publish('first message'); - duplicateNameScope.create('channel').publish('second message'); - - expect(scope1Message.length).toEqual(1); - verifyMessage(scope1Message[0], 'first message'); - - expect(scope2Message.length).toEqual(1); - verifyMessage(scope2Message[0], 'second message'); - }); - describe('Destroy', () => { it('should dispose of all subscriptions on that instance and its child', () => { const instance = getInstance(); const instanceChannel = instance.create('yourChannel'); - const child = instance.createScope('child'); + const child = instance.createScope(); const childChannel = instance.create('yourChannel'); instance.destroy(); // destroy the PARENT @@ -499,45 +455,24 @@ describe('MessageBroker', () => { expect(postDisposeChildChannel).not.toBe(childChannel); }); - it('should remove itself from its parents children', () => { - const parent = getInstance(); - const child = parent.createScope('child'); - - expect(parent.children).toContain(child); - child.destroy(); - expect(parent.children).not.toContain(child); - }); - - it('should remove its parent', () => { - const parent = getInstance(); - const child = parent.createScope('child'); - - child.destroy(); - expect(child.parent).toBeUndefined(); - }); - it('should prevent message propagation from happening', () => { const childMessages: Array> = []; const parentMessages: Array> = []; const parent = getInstance(); - const child = parent.createScope('child'); + const child = parent.createScope(); - child.get('channel').subscribe((message) => childMessages.push(message)); parent.get('channel').subscribe((message) => parentMessages.push(message)); + parent.destroy(); - child.destroy(); - - parent.create('channel').publish('message'); + child.create('channel').publish('message'); expect(childMessages.length).toEqual(0); - - expect(parentMessages.length).toEqual(1); - verifyMessage(parentMessages[0], 'message'); + expect(parentMessages.length).toEqual(0); }); it('should destroy all child scopes as well', () => { const parent = getInstance(); - const child = parent.createScope('child'); + const child = parent.createScope(); const parentChannel = parent.create('channel', { replayCacheSize: 2 }); const childChannel = child.create('channel', { replayCacheSize: 2 }); From 9eab1c236231b6a54b8ca56e459dec0dd55451b6 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 14 Feb 2025 16:20:03 +0000 Subject: [PATCH 13/17] updating documentation --- main/core/messagebroker.ts | 4 +- site/content/documentation/1.0.0/scopes.mdx | 41 +++++++-------------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index 945422a..a0f2eef 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -111,8 +111,8 @@ export class MessageBroker implements IM } /* - * Destroys all children scopes, disposes of all message channels on - * this instance and removes itself from its parents children. + * Disposes of all message channels on this instance. + * It also destroys the connection between this and its parent so that messages will no longer propogate up. */ public destroy(): void { type Channels = (keyof typeof this.channelLookup)[]; diff --git a/site/content/documentation/1.0.0/scopes.mdx b/site/content/documentation/1.0.0/scopes.mdx index c3fd0d5..a89c404 100644 --- a/site/content/documentation/1.0.0/scopes.mdx +++ b/site/content/documentation/1.0.0/scopes.mdx @@ -11,29 +11,25 @@ const parent: IMessageBroker = messagebroker(); const child: IMessageBroker - = parent.createScope('my-scope'); - -parent.children.contains(child) // expect: true + = parent.createScope(); ``` A scope is just another instance of an `IMessageBroker` on which you can perform all of the same operations that you'd expect on the base messagebroker. -The main thing to note about this feature is how messages are shared across scopes. +The main thing to note about this feature is how messages are handled when a scope doesn't have any subscribers for a channel. ### Scope Hierarchies -Any message that is published to a broker is also published down through the hierarchy of children scopes belonging that broker. +Any message that is published to a broker will be passed up the chain of scopes until a handler for the message is found. ```typescript parent.get('x').subscribe(message => console.log('parent received')); -child.get('x').subscribe(message => console.log('child received')); -parent.create('x').publish({}); +child.create('x').publish({}); -// expect: child received // expect: parent received ``` -However messages are not sent **up** the hierarchy to the parent of that broker. +However messages are **not** sent up the hierarchy if the channel in the child has been subscribed to. ```typescript parent.get('x').subscribe(message => console.log('parent received')); @@ -48,7 +44,7 @@ Messages are also not published to "sibling" scopes, where the brokers share a p ```typescript const sibling: IMessageBroker - = parent.createScope('sibling-scope'); + = parent.createScope(); parent.get('x').subscribe(message => console.log('parent received')); child.get('x').subscribe(message => console.log('child received')); @@ -61,33 +57,22 @@ sibling.create('x').publish({}); ### Scope Depth -Scope hierarchies can be arbitrarily deep, and messages will make their way all the way down to the bottom. +Scope hierarchies can be arbitrarily deep, and messages will make their way all the way to the top to find a handler. ```typescript const distantChild = parent - .createScope('scope1') - .createScope('scope2') + .createScope() + .createScope() ... - .createScope('scopeX'); + .createScope(); -distantChild.get('x').subscribe(message => console.log('child received')); +parent.get('x').subscribe(message => console.log('parent received')); -parent.create('x').publish({}); +distantChild.create('x').publish({}); -// expect: child received +// expect: parent received ``` -### Naming - -Scopes under the same parent cannot have the same name. -An attempt to create a scope with a name that already exists on a broker will just return the original scope. - -If they are different parts of the hierarchy (i.e. don't share a parent), then you can have multiple scopes with the same name. - -### Disposal - -Disposing of a channel in a broker will also dispose of that channel in its children scopes. - ### Destroy A MessageBroker instance can be destroyed. From 02199d3ef5d4c643f9953d6a587edad298373d8f Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 14 Feb 2025 16:22:13 +0000 Subject: [PATCH 14/17] updated method docs --- main/contracts/contracts.ts | 13 ++++++------- main/core/messagebroker.ts | 7 +++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/main/contracts/contracts.ts b/main/contracts/contracts.ts index f0dab05..31d838e 100644 --- a/main/contracts/contracts.ts +++ b/main/contracts/contracts.ts @@ -98,16 +98,15 @@ export interface IMessageBroker { 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 + * Creates a new scope with this instance of the MessageBroker as its parent. + * Messages from the scope will be passed to this instance if the child scope doesn't have a handler for it. + * @returns A new instance of the messagebroker */ - createScope(scopeName: string): IMessageBroker; + createScope(): IMessageBroker; /* - * Destroys all children scopes, disposes of all message channels on - * this instance and removes itself from its parents children. + * Disposes of all message channels on this instance. + * It also destroys the connection between this and its parent so that messages will no longer propogate up. */ destroy(): void; diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index a0f2eef..a7b94c8 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -100,10 +100,9 @@ export class MessageBroker implements IM } /** - * 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 + * Creates a new scope with this instance of the MessageBroker as its parent. + * Messages from the scope will be passed to this instance if the child scope doesn't have a handler for it. + * @returns A new instance of the messagebroker */ public createScope(): IMessageBroker { const instance = new MessageBroker(this.rsvpMediator, this as MessageBroker); From 811a6c27edd78ccaa9bccf7250ac4620add6ae7e Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 14 Feb 2025 16:23:23 +0000 Subject: [PATCH 15/17] removed unnecessary type assertion --- main/core/messagebroker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/core/messagebroker.ts b/main/core/messagebroker.ts index a7b94c8..be7dd43 100644 --- a/main/core/messagebroker.ts +++ b/main/core/messagebroker.ts @@ -105,7 +105,7 @@ export class MessageBroker implements IM * @returns A new instance of the messagebroker */ public createScope(): IMessageBroker { - const instance = new MessageBroker(this.rsvpMediator, this as MessageBroker); + const instance = new MessageBroker(this.rsvpMediator, this); return instance; } From f453adb9a583c9c1425984a65c1d4fcf6af3ca74 Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Fri, 14 Feb 2025 16:25:26 +0000 Subject: [PATCH 16/17] improved feature description --- site/content/documentation/1.0.0/scopes.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/documentation/1.0.0/scopes.mdx b/site/content/documentation/1.0.0/scopes.mdx index a89c404..1101dc2 100644 --- a/site/content/documentation/1.0.0/scopes.mdx +++ b/site/content/documentation/1.0.0/scopes.mdx @@ -3,7 +3,7 @@ order: 5 title: Scopes --- -Message scoping is a mechanism for restricting which subscribers will receive a given message. +Scopes are a mechanism for creating a tree of messagebrokers, where a message will be passed up the tree until either a handler is found for the message, or the root of the tree is reached. You can create a new scope on a messagebroker by calling `createScope`. ```typescript From 76f1ac5c91cf7e2253dfaa27f6dcf81c498dd29a Mon Sep 17 00:00:00 2001 From: Aidan McPhelim Date: Tue, 18 Feb 2025 13:43:04 +0000 Subject: [PATCH 17/17] fixed tests --- spec/core/messagebroker.spec.ts | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/spec/core/messagebroker.spec.ts b/spec/core/messagebroker.spec.ts index 0140214..9334dff 100644 --- a/spec/core/messagebroker.spec.ts +++ b/spec/core/messagebroker.spec.ts @@ -147,18 +147,6 @@ describe('MessageBroker', () => { expect(postDisposeNextFunction).not.toBe(channel); }); - it('should dipose of child scope channels as well', () => { - const instance = getInstance(); - const child = instance.createScope(); - const channel = child.create('yourChannel'); - - instance.dispose('yourChannel'); // dispose of the channel in the PARENT - - const postDisposeNextFunction = child.create('yourChannel'); - - expect(postDisposeNextFunction).not.toBe(channel); - }); - it('should allow publishing of channel message without data', () => { const instance = getInstance(); const channel = instance.create('yourChannel'); @@ -462,7 +450,8 @@ describe('MessageBroker', () => { const child = parent.createScope(); parent.get('channel').subscribe((message) => parentMessages.push(message)); - parent.destroy(); + + child.destroy(); child.create('channel').publish('message'); @@ -470,28 +459,24 @@ describe('MessageBroker', () => { expect(parentMessages.length).toEqual(0); }); - it('should destroy all child scopes as well', () => { + it('should destroy all cached messages on parent as well', () => { const parent = getInstance(); const child = parent.createScope(); const parentChannel = parent.create('channel', { replayCacheSize: 2 }); const childChannel = child.create('channel', { replayCacheSize: 2 }); - parentChannel.publish('message one'); - parentChannel.publish('message two'); + childChannel.publish('message one'); + childChannel.publish('message two'); - parent.destroy(); // this should cancel the existing caching subscriptions + child.destroy(); // this should cancel the existing caching subscriptions - const childMessages: Array> = []; const parentMessages: Array> = []; parentChannel.stream.subscribe((message) => parentMessages.push(message)); - childChannel.stream.subscribe((message) => childMessages.push(message)); - parentChannel.publish('message three'); + childChannel.publish('message three'); - expect(childMessages.length).toEqual(0); - expect(parentMessages.length).toEqual(1); - verifyMessage(parentMessages[0], 'message three'); + expect(parentMessages.length).toEqual(0); }); }); });