Promisified Asynchronous APIs
Relevant source files
The following files were used as context for generating this wiki page:
The Ghostbird add-on relies heavily on asynchronous communication between its different components, such as the background script, the compose script, and the external GhostText server. This communication happens over APIs like WebSockets and runtime.Port, which are inherently event-driven. To manage the complexity of these asynchronous operations and avoid "callback hell," the project employs promisified wrappers and utility classes. These utilities provide a modern async/await interface over the traditional event-based APIs.
A central piece of this architecture is the PromisifyingQueue utility, which acts as a bridge between event-driven message producers and promise-based consumers. This allows different parts of the application to cleanly await incoming messages without complex state management and event listener handling. This approach is crucial for the interaction between the background script managing the WebSocket connection and the compose script updating the UI.
Sources: doc/design.md, src/util/promisifying_queue.ts, README.md
PromisifyingQueue
Sources: src/util/promisifying_queue.ts
The PromisifyingQueue<TMessage> class is a generic utility designed to buffer incoming messages and allow consumers to retrieve them using Promise-based methods. It decouples the message receiver (which pushes messages into the queue) from the message processor (which dequeues and handles them).
It manages an internal queue (receiveQueue) for messages and a list of pending promises (pending) for consumers that are waiting for new messages. It also tracks the connection state, allowing it to reject consumers if the underlying connection is closed.
Sources: src/util/promisifying_queue.ts:6-8
Key Components and State
The queue's behavior is governed by a few key internal properties:
receiveQueue: An array ofTMessagethat holds the buffered messages.pending: An array ofPromiseWithResolvers<void>for consumers awaiting a message viawaitReady().disconnectError: AnErrorobject that, when set, signifies the queue is closed. All subsequent and pending operations will be rejected with this error.
Sources: src/util/promisifying_queue.ts:6-8
The following diagram illustrates the state transitions of the PromisifyingQueue.
This state diagram shows the two main states of the queue and the methods that cause transitions or actions within those states.
Sources: src/util/promisifying_queue.ts
Public API
The class exposes a simple but powerful API for managing the asynchronous message flow.
| Method | Description |
|---|---|
isOpen(): boolean | Checks if the queue is open (i.e., disconnectError is not set). |
pushReceived(msg: TMessage): void | Adds a message to the queue and resolves any pending waitReady() promises. |
notifyClosed(err: Error): void | Closes the queue, setting a disconnection error and rejecting all pending and future consumers. |
waitReady(): Promise<void> | Returns a Promise that resolves when there are messages in the queue. If the queue is empty, it waits. If the queue is closed, it rejects. |
dequeueReceived(): TMessage | undefined | Removes and returns the first message from the queue. Returns undefined if empty. Throws if the queue is closed. |
clearReceived(): TMessage | undefined | Clears all messages from the queue and returns the last one. Returns undefined if empty. Throws if the queue is closed. |
Sources: src/util/promisifying_queue.ts:15-95
Interaction Flow
The sequence diagram below shows a typical interaction between a message producer and a consumer using the PromisifyingQueue.
This diagram illustrates how a consumer can wait for a message, and a producer can add a message that fulfills the consumer's wait.
Sources: src/util/promisifying_queue.ts:22-31, 51-62, 71-77
Architectural Context and Usage
Sources: doc/design.md, README.md, doc/faq.md
In Ghostbird, the primary asynchronous operations involve communication between the background.js script, the compose.js script, and the external GhostText WebSocket server. The doc/design.md file outlines this architecture, highlighting the roles of runtime.Port for internal communication and WebSocket for external communication.
The diagram below, from the project's README.md, shows the high-level data flow.
This flow relies on robust handling of messages from both the WebSocket (from gtServer) and the runtime.Port (between gtClient components).
Sources: README.md, doc/design.md
While the source code for specific wrappers like PromisifiedWebSocket and PromisifiedMessagePort is not available in the provided context, their names suggest they are wrappers around the native WebSocket and runtime.Port APIs. These wrappers likely use a utility like PromisifyingQueue internally to manage incoming messages and provide a clean async/await interface for reading data, simplifying the logic in the background.js and compose.js scripts.
The sequence diagram from the design documentation further clarifies this interaction.
In this flow, background.js (B) must listen for messages from both the WebSocket (S) and the Port (C). A promisified API, likely backed by PromisifyingQueue, would allow the background script to use an async iterator or a while loop with await to process messages from these sources, rather than managing multiple onmessage event handlers.
Sources: doc/design.md
Conclusion
The use of promisified wrappers and queuing mechanisms like PromisifyingQueue is a key architectural pattern in Ghostbird. It transforms complex, event-driven I/O operations into linear, understandable, and maintainable code using modern JavaScript async/await syntax. This abstraction is fundamental to managing the flow of data between the Thunderbird UI, the extension's background processes, and the external GhostText server, ensuring a robust and responsive user experience.
Sources: src/util/promisifying_queue.ts, doc/design.md