@morgan-stanley/composeui-messaging-message-router 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@morgan-stanley/composeui-messaging-message-router",
3
+ "version": "0.1.0-alpha.10",
4
+ "description": "TypeScript messaging implementation for ComposeUI. It uses the MessageRouter client library.",
5
+ "private": false,
6
+ "author": "Morgan Stanley",
7
+ "license": "Apache-2.0",
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.js",
11
+ "scripts": {
12
+ "clean:dist": "rimraf dist",
13
+ "clean:output": "rimraf output",
14
+ "clean": "npm run clean:dist && npm run clean:output",
15
+ "bundle": "rollup -c",
16
+ "build": "npm run clean && tsc && npm run bundle",
17
+ "test": "vitest run"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "provenance": true
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/morganstanley/ComposeUI.git"
26
+ },
27
+ "dependencies": {
28
+ "@morgan-stanley/composeui-messaging-abstractions": "*",
29
+ "@morgan-stanley/composeui-messaging-client": "*",
30
+ "rxjs": "^7.8.0"
31
+ },
32
+ "devDependencies": {
33
+ "@rollup/plugin-commonjs": "28.0.6",
34
+ "@rollup/plugin-node-resolve": "16.0.1",
35
+ "@types/node": "^24.5.2",
36
+ "jsdom": "^26.0.0",
37
+ "rimraf": "6.0.1",
38
+ "rollup": "^4.12.1",
39
+ "tslib": "^2.4.0",
40
+ "typescript": "^5.0.0",
41
+ "vitest": "^2.0.0"
42
+ },
43
+ "gitHead": "4dc7ce6e9965a0b0a772e8d9b8f3cf348ba875df"
44
+ }
@@ -0,0 +1,16 @@
1
+ import resolve from '@rollup/plugin-node-resolve';
2
+ import commonjs from '@rollup/plugin-commonjs';
3
+
4
+
5
+ export default {
6
+ input: 'output/index.js',
7
+ output: {
8
+ file: 'dist/messageRouterMessaging-iife-bundle.js',
9
+ format: 'iife',
10
+ name: 'messageRouterMessaging'
11
+ },
12
+ plugins: [
13
+ resolve(),
14
+ commonjs()
15
+ ]
16
+ };
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Morgan Stanley makes this available to you under the Apache License,
3
+ * Version 2.0 (the "License"). You may obtain a copy of the License at
4
+ * http://www.apache.org/licenses/LICENSE-2.0.
5
+ * See the NOTICE file distributed with this work for additional information
6
+ * regarding copyright ownership. Unless required by applicable law or agreed
7
+ * to in writing, software distributed under the License is distributed on an
8
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
9
+ * or implied. See the License for the specific language governing permissions
10
+ * and limitations under the License.
11
+ *
12
+ */
13
+ import { MessageRouter } from "@morgan-stanley/composeui-messaging-client";
14
+ export class AsyncDisposableWrapper implements AsyncDisposable {
15
+ constructor(private readonly messageRouterClient: MessageRouter, private readonly serviceName: string) {}
16
+
17
+ public [Symbol.asyncDispose](): PromiseLike<void> {
18
+ return this.messageRouterClient.unregisterService(this.serviceName);
19
+ }
20
+ }
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { MessageRouterMessaging } from './MessageRouterMessaging';
3
+ import { MessageRouter } from "@morgan-stanley/composeui-messaging-client";
4
+ import { TopicMessageHandler } from "@morgan-stanley/composeui-messaging-abstractions";
5
+
6
+ describe('MessageRouterMessaging', () => {
7
+ let mockMessageRouter: MessageRouter;
8
+ let messaging: MessageRouterMessaging;
9
+
10
+ beforeEach(() => {
11
+ mockMessageRouter = {
12
+ subscribe: vi.fn(),
13
+ publish: vi.fn(),
14
+ registerService: vi.fn(),
15
+ invoke: vi.fn(),
16
+ unregisterService: vi.fn()
17
+ } as any;
18
+
19
+ messaging = new MessageRouterMessaging(mockMessageRouter);
20
+ });
21
+
22
+ describe('subscribe', () => {
23
+ it('should delegate subscribe to message router', async () => {
24
+ const topic = 'test-topic';
25
+ const handler: TopicMessageHandler = vi.fn();
26
+ const unsubscribe = { unsubscribe: vi.fn() };
27
+
28
+ (mockMessageRouter.subscribe as any).mockResolvedValue(unsubscribe);
29
+
30
+ const result = await messaging.subscribe(topic, handler);
31
+
32
+ expect(mockMessageRouter.subscribe).toHaveBeenCalledWith(topic, expect.any(Function));
33
+ expect(result).toBe(unsubscribe);
34
+ });
35
+
36
+ it('should handle empty messages', async () => {
37
+ const topic = 'test-topic';
38
+ const handler = vi.fn();
39
+ let storedCallback: Function = () => {};
40
+
41
+ (mockMessageRouter.subscribe as any).mockImplementation((t: string, callback: Function) => {
42
+ storedCallback = callback;
43
+ return Promise.resolve({ unsubscribe: vi.fn() });
44
+ });
45
+
46
+ await messaging.subscribe(topic, handler);
47
+ await storedCallback({ payload: null });
48
+
49
+ expect(handler).not.toHaveBeenCalled();
50
+ });
51
+ });
52
+
53
+ describe('publish', () => {
54
+ it('should delegate publish to message router', async () => {
55
+ const topic = 'test-topic';
56
+ const message = 'test-message';
57
+
58
+ await messaging.publish(topic, message);
59
+
60
+ expect(mockMessageRouter.publish).toHaveBeenCalledWith(topic, message);
61
+ });
62
+ });
63
+
64
+ describe('registerService', () => {
65
+ it('should delegate service registration to message router', async () => {
66
+ const serviceName = 'test-service';
67
+ const handler = vi.fn().mockResolvedValue('response');
68
+
69
+ const disposable = await messaging.registerService(serviceName, handler);
70
+
71
+ expect(mockMessageRouter.registerService).toHaveBeenCalledWith(
72
+ serviceName,
73
+ expect.any(Function)
74
+ );
75
+
76
+ expect(disposable).toBeDefined();
77
+ expect(typeof disposable[Symbol.asyncDispose]).toBe('function');
78
+ });
79
+
80
+ it('should handle service calls correctly', async () => {
81
+ const serviceName = 'test-service';
82
+ const handler = vi.fn().mockResolvedValue('response');
83
+ let registeredHandler: Function | undefined;
84
+
85
+ (mockMessageRouter.registerService as any).mockImplementation((name: string, callback: Function) => {
86
+ registeredHandler = callback;
87
+ return Promise.resolve();
88
+ });
89
+
90
+ await messaging.registerService(serviceName, handler);
91
+
92
+ if (!registeredHandler) {
93
+ throw new Error('Handler was not registered');
94
+ }
95
+
96
+ const result = await registeredHandler('endpoint', 'payload', { context: 'test' });
97
+
98
+ expect(handler).toHaveBeenCalledWith('payload');
99
+ expect(result).toBe('response');
100
+ });
101
+
102
+ it('should handle null payload in service call', async () => {
103
+ const serviceName = 'test-service';
104
+ const handler = vi.fn().mockResolvedValue('response');
105
+ let registeredHandler: Function | undefined;
106
+
107
+ (mockMessageRouter.registerService as any).mockImplementation((name: string, callback: Function) => {
108
+ registeredHandler = callback;
109
+ return Promise.resolve();
110
+ });
111
+
112
+ await messaging.registerService(serviceName, handler);
113
+
114
+ if (!registeredHandler) {
115
+ throw new Error('Handler was not registered');
116
+ }
117
+
118
+ const result = await registeredHandler('endpoint', null, { context: 'test' });
119
+
120
+ expect(handler).toHaveBeenCalledWith(null);
121
+ expect(result).toBe('response');
122
+ });
123
+ });
124
+
125
+ describe('invokeService', () => {
126
+ it('should handle service invocation with payload', async () => {
127
+ const serviceName = 'test-service';
128
+ const payload = 'test-payload';
129
+ const response = 'test-response';
130
+
131
+ (mockMessageRouter.invoke as any).mockResolvedValue(response);
132
+
133
+ const result = await messaging.invokeService(serviceName, payload);
134
+
135
+ expect(mockMessageRouter.invoke).toHaveBeenCalledWith(serviceName, payload);
136
+ expect(result).toBe(response);
137
+ });
138
+
139
+ it('should handle service invocation without payload', async () => {
140
+ const serviceName = 'test-service';
141
+ const response = 'test-response';
142
+
143
+ (mockMessageRouter.invoke as any).mockResolvedValue(response);
144
+
145
+ const result = await messaging.invokeService(serviceName);
146
+
147
+ expect(mockMessageRouter.invoke).toHaveBeenCalledWith(serviceName);
148
+ expect(result).toBe(response);
149
+ });
150
+
151
+ it('should handle null response', async () => {
152
+ const serviceName = 'test-service';
153
+
154
+ (mockMessageRouter.invoke as any).mockResolvedValue(null);
155
+
156
+ const result = await messaging.invokeService(serviceName, 'payload');
157
+
158
+ expect(result).toBeNull();
159
+ });
160
+ });
161
+
162
+ describe('AsyncDisposableWrapper', () => {
163
+ it('should unregister service on dispose', async () => {
164
+ const serviceName = 'test-service';
165
+ const handler = vi.fn();
166
+
167
+ const disposable = await messaging.registerService(serviceName, handler);
168
+ await disposable[Symbol.asyncDispose]();
169
+
170
+ expect(mockMessageRouter.unregisterService).toHaveBeenCalledWith(serviceName);
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Morgan Stanley makes this available to you under the Apache License,
3
+ * Version 2.0 (the "License"). You may obtain a copy of the License at
4
+ * http://www.apache.org/licenses/LICENSE-2.0.
5
+ * See the NOTICE file distributed with this work for additional information
6
+ * regarding copyright ownership. Unless required by applicable law or agreed
7
+ * to in writing, software distributed under the License is distributed on an
8
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
9
+ * or implied. See the License for the specific language governing permissions
10
+ * and limitations under the License.
11
+ *
12
+ */
13
+
14
+ import { IMessaging, ServiceHandler, TopicMessageHandler } from "@morgan-stanley/composeui-messaging-abstractions";
15
+ import { MessageRouter } from "@morgan-stanley/composeui-messaging-client";
16
+ import { Unsubscribable } from "rxjs";
17
+ import { AsyncDisposableWrapper } from "./AsyncDisposableWrapper";
18
+
19
+ /**
20
+ * Implementation of IMessaging interface using MessageRouter.
21
+ * Provides messaging capabilities through the MessageRouter client for ComposeUI applications.
22
+ */
23
+ export class MessageRouterMessaging implements IMessaging {
24
+ /**
25
+ * Creates a new instance of MessageRouterMessaging.
26
+ * @param messageRouterClient The MessageRouter client instance to use for communication.
27
+ */
28
+ constructor(private readonly messageRouterClient: MessageRouter) {
29
+ }
30
+
31
+ /**
32
+ * Subscribes to messages on a specific topic.
33
+ * @param topic The topic to subscribe to.
34
+ * @param subscriber Callback function that will be invoked with each received message.
35
+ * @param cancellationToken Optional signal to cancel the subscription setup.
36
+ * @returns A Promise that resolves to an Unsubscribable object for managing the subscription.
37
+ * @remarks If a message is received without a payload, a warning will be logged and the subscriber will not be called.
38
+ */
39
+ public subscribe(topic: string, subscriber: TopicMessageHandler, cancellationToken?: AbortSignal): Promise<Unsubscribable> {
40
+
41
+ return this.messageRouterClient.subscribe(topic, (message) => {
42
+ if (!message.payload) {
43
+ console.warn(`Received empty message on topic ${topic}`);
44
+ return;
45
+ }
46
+ subscriber(message.payload);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Publishes a message to a specific topic.
52
+ * @param topic The topic to publish to.
53
+ * @param message The message content to publish.
54
+ * @param cancellationToken Optional signal to cancel the publish operation.
55
+ * @returns A Promise that resolves when the message has been published.
56
+ */
57
+ public publish(topic: string, message: string, cancellationToken?: AbortSignal): Promise<void> {
58
+ return this.messageRouterClient.publish(topic, message);
59
+ }
60
+
61
+ /**
62
+ * Registers a service handler for a specific service name.
63
+ * @param serviceName The name of the service to register.
64
+ * @param serviceHandler The handler function that will process service requests.
65
+ * @param cancellationToken Optional signal to cancel the service registration.
66
+ * @returns A Promise that resolves to an AsyncDisposable for managing the service registration.
67
+ * @remarks The service handler will receive the payload from the request and should return a response.
68
+ * Both the payload and response can be null.
69
+ */
70
+ public async registerService(serviceName: string, serviceHandler: ServiceHandler, cancellationToken?: AbortSignal): Promise<AsyncDisposable> {
71
+ await this.messageRouterClient.registerService(serviceName, async (endpoint, payload, context) => {
72
+ const result = await serviceHandler(payload!);
73
+ return result!;
74
+ });
75
+
76
+ const disposable = new AsyncDisposableWrapper(this.messageRouterClient, serviceName);
77
+ return disposable;
78
+ }
79
+
80
+ /**
81
+ * Invokes a registered service.
82
+ * @param serviceName The name of the service to invoke.
83
+ * @param payload Optional payload to send with the service request.
84
+ * @param cancellationToken Optional signal to cancel the service invocation.
85
+ * @returns A Promise that resolves to the service response or null if no response is received.
86
+ * @remarks If the payload is null, the service will be invoked without a payload.
87
+ * The response will be null if the service doesn't return a response or if an error occurs.
88
+ */
89
+ public async invokeService(serviceName: string, payload?: string | null, cancellationToken?: AbortSignal): Promise<string | null> {
90
+ if (payload == null) {
91
+ const result = await this.messageRouterClient.invoke(serviceName);
92
+ if (!result){
93
+ return null;
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ const response = await this.messageRouterClient.invoke(serviceName, payload);
100
+ if (!response) {
101
+ return null;
102
+ }
103
+ return response;
104
+ }
105
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Morgan Stanley makes this available to you under the Apache License,
3
+ * Version 2.0 (the "License"). You may obtain a copy of the License at
4
+ * http://www.apache.org/licenses/LICENSE-2.0.
5
+ * See the NOTICE file distributed with this work for additional information
6
+ * regarding copyright ownership. Unless required by applicable law or agreed
7
+ * to in writing, software distributed under the License is distributed on an
8
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
9
+ * or implied. See the License for the specific language governing permissions
10
+ * and limitations under the License.
11
+ *
12
+ */
13
+ import { IMessaging } from "@morgan-stanley/composeui-messaging-abstractions";
14
+ import { MessageRouterMessaging } from "./MessageRouterMessaging";
15
+ import { createMessageRouter } from "@morgan-stanley/composeui-messaging-client";
16
+
17
+ declare global {
18
+ interface Window {
19
+ composeui: {
20
+ messaging: {
21
+ communicator: IMessaging | undefined;
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ window.composeui.messaging.communicator = new MessageRouterMessaging(createMessageRouter());
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "outDir": "output",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "**/*.test.ts"]
14
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ })