diff --git a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts index 9489c5f2a4e..c45dca88707 100644 --- a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts +++ b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts @@ -1,9 +1,6 @@ import { map, share } from "rxjs"; -import { Message } from "@bitwarden/common/platform/messaging"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; +import { Message, tagAsExternal } from "@bitwarden/messaging"; import { fromChromeEvent } from "../browser/from-chrome-event"; diff --git a/apps/desktop/src/platform/utils/from-ipc-messaging.ts b/apps/desktop/src/platform/utils/from-ipc-messaging.ts index f9c01c9487f..ffdbd4dfd0f 100644 --- a/apps/desktop/src/platform/utils/from-ipc-messaging.ts +++ b/apps/desktop/src/platform/utils/from-ipc-messaging.ts @@ -1,9 +1,6 @@ import { fromEventPattern, share } from "rxjs"; -import { Message } from "@bitwarden/common/platform/messaging"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; +import { Message, tagAsExternal } from "@bitwarden/messaging"; /** * Creates an observable that when subscribed to will listen to messaging events through IPC. diff --git a/libs/messaging/README.md b/libs/messaging/README.md index 98eb96a5a40..8b299ac1366 100644 --- a/libs/messaging/README.md +++ b/libs/messaging/README.md @@ -3,3 +3,93 @@ Owned by: platform Services for sending and recieving messages from different contexts of the same application. + +## Usage + +The best way to use messaging is along with a `CommandDefinition` to help encourage type safety on +either side of the message. An overload of `MessageSender.send` exists that takes a `string` for the +command and a `Record` for the payload, this exists for backwards compatibility but +all new uses are highly encouraged to use the `CommandDefinition` overload. + +```typescript +import { MessageSender, MessageListener, CommandDefinition } from "@bitwarden/messaging"; + +const MY_MESSAGE = new CommandDefinition<{ data: number }>("myMessage"); +const MY_EVENT = new CommandDefinition<{ name: string }>("myEvent"); + +class MyService { + myEvent$: Observable<{ name: string }>; + + contructor(private messageSender: MessageSender, private messageListener: MessageListener) { + this.myEvent$ = this.messageListener.messages$(MY_EVENT); + } + + async doThing() { + this.messageSender.send(MY_MESSAGE, { data: 5 }); + } +} +``` + +## Implementation + +### MessageSender + +MessageSender has 4 implementations to help sending messages to all the different contexts of a +Bitwarden application. + +#### SubjectMessageSender + +The `SubjectMessageSender` takes an rxjs `Subject` and nexts the given payload along with a +`command` property with the given command name. + +#### MultiMessageSender + +This implementation just takes an array of other `MessageSender` implementations and sends all the +messages it gets to all of those implementations. This exists especially so we can use a +`SubjectMessageSender` to handle messages from the same context and use another client specific +implementation to handle messages with another context. + +#### ChromeMessageSender + +This implementation uses [`chrome.runtime.sendMessage`][chrome-runtime-sendMessage] to send messages +to other contexts of our browser extension. This API is the only one of the bunch that is +asynchronous under the hood but since the `send` method forces all `MessageSender`s to be sync. We +take care of this by handling the promise through logging. + +### ElectronMainMessageSender + +This implementation sends messages through the electron +[`BrowserWindow.webContents.send`][electron-send] API. This makes the message available through +listeners added in the `ipcRenderer.addListener` API. + +### MessageListener + +#### Observable + +There is only one implementation implementation of `MessageListener` and it just takes an observable +of all messages. Similar to how `MultiMessageSender` sends messages to multiple sources this +listener can listen to multiple sources by using the RXJS operator `merge` to listen to multiple +observable sources of messages. So far all of our message sources are able to be converted pretty +easily to observables using some type of `fromEventPattern` function. + +#### Subject + +The same subject that is passed to `SubjectMessageSender` can also be converted to an observable +using `asObservable()`. + +#### `chrome.runtime.onMessage` + +For browser, We use the `chrome.runtime.onMessage` API and convert it to an observable using +`fromChromeEvent`. Messages from this source are also tagged as being an external message (meaning +not the same exact context) such that consumers could filter them out using +`filter(isExternalMessage)`. When messages are read from this API in Angular contexts we also use +our `runInsideAngular` operator to force the messages into the Angular zone so that messages are +able to affect the UI without every consumer doing that themselves. + +#### IPC + +Similar to the extensions implementation the browser renderer process wraps `ipc.platform.onMessage` +to convert those messages into an observable. + +[chrome-runtime-sendMessage]: [https://developer.chrome.com/docs/extensions/reference/api/runtime#method-sendMessage] +[electron-send]: [https://www.electronjs.org/docs/latest/api/web-contents#contentssendchannel-args] diff --git a/libs/messaging/src/message.sender.ts b/libs/messaging/src/message.sender.ts index fc8ad450746..f7e578b19f3 100644 --- a/libs/messaging/src/message.sender.ts +++ b/libs/messaging/src/message.sender.ts @@ -8,7 +8,9 @@ class MultiMessageSender implements MessageSender { payload: Record | T = {}, ): void { for (const messageSender of this.innerMessageSenders) { - messageSender.send(commandDefinition, payload); + // This is technically an invalid cast but we just have to tell TypeScript + // it's one of the two so its okay with us passing the value along. + messageSender.send(commandDefinition as CommandDefinition, payload); } } } @@ -43,12 +45,6 @@ export abstract class MessageSender { */ abstract send(command: string, payload?: Record): void; - /** Implementation of the other two overloads, read their docs instead. */ - abstract send>( - commandDefinition: CommandDefinition | string, - payload: T | Record, - ): void; - /** * A helper method for combine multiple {@link MessageSender}'s. * @param messageSenders The message senders that should be combined.