diff --git a/packages/amqp/README.md b/packages/amqp/README.md index 7d1b9abc..16163a45 100644 --- a/packages/amqp/README.md +++ b/packages/amqp/README.md @@ -2,8 +2,7 @@ The library provides support for both direct exchanges and topic exchanges. -> **_NOTE:_** Check [README.md](../../README.md) for transport-agnostic library documentation. -> +> **_NOTE:_** Check [README.md](../../README.md) for transport-agnostic library documentation. ## Publishers @@ -25,42 +24,44 @@ Example: ```ts export const TEST_AMQP_CONFIG: AmqpConfig = { - vhost: '', - hostname: 'localhost', - username: 'guest', - password: 'guest', - port: 5672, - useTls: false, + vhost: '', + hostname: 'localhost', + username: 'guest', + password: 'guest', + port: 5672, + useTls: false, } const amqpConnectionManager = new AmqpConnectionManager(config, logger) await amqpConnectionManager.init() const publisher = new TestAmqpPublisher( - { amqpConnectionManager }, - { - // other amqp options - }) + { amqpConnectionManager }, + { + // other amqp options + }, +) await publisher.init() const consumer = new TestAmqpConsumer( - { amqpConnectionManager }, - { - // other amqp options - }) + { amqpConnectionManager }, + { + // other amqp options + }, +) await consumer.start() // break connection, to simulate unexpected disconnection in production await (await amqpConnectionManager.getConnection()).close() -const message = { - // some test message +const message = { + // some test message } // This will fail, but will trigger reconnection within amqpConnectionManager publisher.publish(message) - -// eventually connection is reestablished and propagated across all the AMQP services that use same amqpConnectionManager + +// eventually connection is reestablished and propagated across all the AMQP services that use same amqpConnectionManager // This will succeed and consumer, which also received new connection, will be able to consume it publisher.publish(message) diff --git a/packages/amqp/lib/AbstractAmqpConsumer.ts b/packages/amqp/lib/AbstractAmqpConsumer.ts index a27a4621..5ff32a0f 100644 --- a/packages/amqp/lib/AbstractAmqpConsumer.ts +++ b/packages/amqp/lib/AbstractAmqpConsumer.ts @@ -8,13 +8,9 @@ import type { QueueConsumer, QueueConsumerOptions, TransactionObservabilityManager, -} from '@message-queue-toolkit/core' -import { - isMessageError, - parseMessage, - HandlerContainer, MessageSchemaContainer, } from '@message-queue-toolkit/core' +import { isMessageError, parseMessage, HandlerContainer } from '@message-queue-toolkit/core' import type { Connection, Message } from 'amqplib' import type { @@ -100,11 +96,8 @@ export abstract class AbstractAmqpConsumer< ? options.locatorConfig.queueName : options.creationConfig!.queueName - const messageSchemas = options.handlers.map((entry) => entry.schema) - this.messageSchemaContainer = new MessageSchemaContainer({ - messageSchemas, - messageTypeField: options.messageTypeField, - }) + this.messageSchemaContainer = this.resolveConsumerMessageSchemaContainer(options) + this.handlerContainer = new HandlerContainer< MessagePayloadType, ExecutionContext, diff --git a/packages/amqp/lib/AbstractAmqpPublisher.ts b/packages/amqp/lib/AbstractAmqpPublisher.ts index 966f8225..a933c2fa 100644 --- a/packages/amqp/lib/AbstractAmqpPublisher.ts +++ b/packages/amqp/lib/AbstractAmqpPublisher.ts @@ -4,11 +4,12 @@ import type { BarrierResult, CommonCreationConfigType, MessageInvalidFormatError, + MessageSchemaContainer, MessageValidationError, QueuePublisherOptions, SyncPublisher, } from '@message-queue-toolkit/core' -import { objectToBuffer, MessageSchemaContainer } from '@message-queue-toolkit/core' +import { objectToBuffer } from '@message-queue-toolkit/core' import type { ZodSchema } from 'zod' import type { AMQPDependencies } from './AbstractAmqpService' @@ -49,11 +50,7 @@ export abstract class AbstractAmqpPublisher< ) { super(dependencies, options) - const messageSchemas = options.messageSchemas - this.messageSchemaContainer = new MessageSchemaContainer({ - messageSchemas, - messageTypeField: options.messageTypeField, - }) + this.messageSchemaContainer = this.resolvePublisherMessageSchemaContainer(options) this.exchange = options.exchange } diff --git a/packages/core/lib/queues/AbstractQueueService.ts b/packages/core/lib/queues/AbstractQueueService.ts index baea2535..3a428eab 100644 --- a/packages/core/lib/queues/AbstractQueueService.ts +++ b/packages/core/lib/queues/AbstractQueueService.ts @@ -2,6 +2,7 @@ import { types } from 'node:util' import type { ErrorReporter, ErrorResolver, Either } from '@lokalise/node-core' import { resolveGlobalErrorLogObject } from '@lokalise/node-core' +import type { CommonEventDefinition } from '@message-queue-toolkit/schemas' import type { ZodSchema, ZodType } from 'zod' import type { MessageInvalidFormatError, MessageValidationError } from '../errors/Errors' @@ -13,12 +14,14 @@ import { toDatePreprocessor } from '../utils/toDateProcessor' import type { BarrierCallback, BarrierResult, + MessageHandlerConfig, Prehandler, PrehandlerResult, PreHandlingOutputs, } from './HandlerContainer' import type { HandlerSpy, PublicHandlerSpy } from './HandlerSpy' import { resolveHandlerSpy } from './HandlerSpy' +import { MessageSchemaContainer } from './MessageSchemaContainer' export type Deserializer = ( message: unknown, @@ -90,6 +93,37 @@ export abstract class AbstractQueueService< this.isInitted = false } + protected resolveConsumerMessageSchemaContainer(options: { + handlers: MessageHandlerConfig[] + messageTypeField: string + }) { + const messageSchemas = options.handlers.map((entry) => entry.schema) + // @ts-expect-error This should no longer be necessary in upcoming TypeScript updates, filter will narrow down the type + const messageDefinitions: CommonEventDefinition[] = options.handlers + .map((entry) => entry.definition) + .filter((entry) => entry !== undefined) + + return new MessageSchemaContainer({ + messageSchemas, + messageDefinitions, + messageTypeField: options.messageTypeField, + }) + } + + protected resolvePublisherMessageSchemaContainer(options: { + messageSchemas: readonly ZodSchema[] + messageTypeField: string + }) { + const messageSchemas = options.messageSchemas + const messageDefinitions: readonly CommonEventDefinition[] = [] + + return new MessageSchemaContainer({ + messageSchemas, + messageDefinitions, + messageTypeField: options.messageTypeField, + }) + } + protected abstract resolveSchema( message: MessagePayloadSchemas, ): Either> @@ -128,14 +162,10 @@ export abstract class AbstractQueueService< protected handleError(err: unknown, context?: Record) { const logObject = resolveGlobalErrorLogObject(err) - if (logObject === 'string') { - this.logger.error(context, logObject) - } else if (typeof logObject === 'object') { - this.logger.error({ - ...logObject, - ...context, - }) - } + this.logger.error({ + ...logObject, + ...context, + }) if (types.isNativeError(err)) { this.errorReporter.report({ error: err, context }) } diff --git a/packages/core/lib/queues/HandlerContainer.ts b/packages/core/lib/queues/HandlerContainer.ts index 93e16024..c46aac3a 100644 --- a/packages/core/lib/queues/HandlerContainer.ts +++ b/packages/core/lib/queues/HandlerContainer.ts @@ -1,4 +1,6 @@ import type { Either } from '@lokalise/node-core' +import type { CommonEventDefinition } from '@message-queue-toolkit/schemas' +import { isCommonEventDefinition } from '@message-queue-toolkit/schemas' import type { ZodSchema } from 'zod' import type { DoNotProcessMessageError } from '../errors/DoNotProcessError' @@ -70,6 +72,7 @@ export class MessageHandlerConfig< const BarrierOutput = unknown, > { public readonly schema: ZodSchema + public readonly definition?: CommonEventDefinition public readonly handler: Handler< MessagePayloadSchema, ExecutionContext, @@ -98,8 +101,10 @@ export class MessageHandlerConfig< PrehandlerOutput, BarrierOutput >, + eventDefinition?: CommonEventDefinition, ) { this.schema = schema + this.definition = eventDefinition this.handler = handler this.messageLogFormatter = options?.messageLogFormatter ?? defaultLogFormatter this.preHandlerBarrier = options?.preHandlerBarrier @@ -125,7 +130,7 @@ export class MessageHandlerConfigBuilder< } addConfig( - schema: ZodSchema, + schema: ZodSchema | CommonEventDefinition, handler: Handler, options?: HandlerConfigOptions< MessagePayloadSchema, @@ -134,6 +139,12 @@ export class MessageHandlerConfigBuilder< BarrierOutput >, ) { + const resolvedSchema: ZodSchema = isCommonEventDefinition(schema) + ? // @ts-ignore + (schema.consumerSchema as ZodSchema) + : schema + const definition = isCommonEventDefinition(schema) ? schema : undefined + this.configs.push( // @ts-ignore new MessageHandlerConfig< @@ -142,10 +153,11 @@ export class MessageHandlerConfigBuilder< PrehandlerOutput, BarrierOutput >( - schema, + resolvedSchema, // @ts-ignore handler, options, + definition, ), ) return this @@ -165,6 +177,7 @@ export type Handler< message: MessagePayloadSchemas, context: ExecutionContext, preHandlingOutputs: PreHandlingOutputs, + definition?: CommonEventDefinition, ) => Promise> export type HandlerContainerOptions< diff --git a/packages/core/lib/queues/MessageSchemaContainer.ts b/packages/core/lib/queues/MessageSchemaContainer.ts index fcb82d97..1adb7808 100644 --- a/packages/core/lib/queues/MessageSchemaContainer.ts +++ b/packages/core/lib/queues/MessageSchemaContainer.ts @@ -1,18 +1,22 @@ import type { Either } from '@lokalise/node-core' +import type { CommonEventDefinition } from '@message-queue-toolkit/schemas' import type { ZodSchema } from 'zod' export type MessageSchemaContainerOptions = { + messageDefinitions: readonly CommonEventDefinition[] messageSchemas: readonly ZodSchema[] messageTypeField: string } export class MessageSchemaContainer { + public readonly messageDefinitions: Record private readonly messageSchemas: Record> private readonly messageTypeField: string constructor(options: MessageSchemaContainerOptions) { this.messageTypeField = options.messageTypeField this.messageSchemas = this.resolveSchemaMap(options.messageSchemas) + this.messageDefinitions = this.resolveDefinitionsMap(options.messageDefinitions ?? []) } public resolveSchema( @@ -42,4 +46,17 @@ export class MessageSchemaContainer { {} as Record>, ) } + + private resolveDefinitionsMap( + supportedDefinitions: readonly CommonEventDefinition[], + ): Record { + return supportedDefinitions.reduce( + (acc, definition) => { + // @ts-ignore + acc[definition.publisherSchema.shape[this.messageTypeField].value] = definition + return acc + }, + {} as Record, + ) + } } diff --git a/packages/core/package.json b/packages/core/package.json index 0e46b6cf..9da537bf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,7 +25,7 @@ "prepublishOnly": "npm run build:release" }, "dependencies": { - "@lokalise/node-core": "^9.17.0", + "@lokalise/node-core": "^9.21.0", "@message-queue-toolkit/schemas": "^1.0.0", "fast-equals": "^5.0.1", "toad-cache": "^3.7.0", diff --git a/packages/schemas/lib/events/eventTypes.ts b/packages/schemas/lib/events/eventTypes.ts index 21356d1d..f9becdf1 100644 --- a/packages/schemas/lib/events/eventTypes.ts +++ b/packages/schemas/lib/events/eventTypes.ts @@ -8,6 +8,10 @@ import type { CONSUMER_BASE_EVENT_SCHEMA, PUBLISHER_BASE_EVENT_SCHEMA } from './ export type EventTypeNames = CommonEventDefinitionConsumerSchemaType['type'] +export function isCommonEventDefinition(entity: unknown): entity is CommonEventDefinition { + return (entity as CommonEventDefinition).publisherSchema !== undefined +} + export type CommonEventDefinition = { consumerSchema: ZodObject< Omit<(typeof CONSUMER_BASE_EVENT_SCHEMA)['shape'], 'payload'> & { payload: ZodTypeAny } @@ -16,6 +20,13 @@ export type CommonEventDefinition = { Omit<(typeof PUBLISHER_BASE_EVENT_SCHEMA)['shape'], 'payload'> & { payload: ZodTypeAny } > schemaVersion?: string + + // + // Metadata used for automated documentation generation + // + producedBy?: readonly string[] // Service ids for all the producers of this event. + domain?: string // Domain of the event + tags?: readonly string[] // Free-form tags for the event } export type CommonEventDefinitionConsumerSchemaType = z.infer< diff --git a/packages/sns/lib/sns/AbstractSnsPublisher.ts b/packages/sns/lib/sns/AbstractSnsPublisher.ts index 24086ef9..9c2e83e3 100644 --- a/packages/sns/lib/sns/AbstractSnsPublisher.ts +++ b/packages/sns/lib/sns/AbstractSnsPublisher.ts @@ -8,8 +8,8 @@ import type { MessageInvalidFormatError, MessageValidationError, QueuePublisherOptions, + MessageSchemaContainer, } from '@message-queue-toolkit/core' -import { MessageSchemaContainer } from '@message-queue-toolkit/core' import type { SNSCreationConfig, SNSDependencies, SNSQueueLocatorType } from './AbstractSnsService' import { AbstractSnsService } from './AbstractSnsService' @@ -36,11 +36,7 @@ export abstract class AbstractSnsPublisher constructor(dependencies: SNSDependencies, options: SNSPublisherOptions) { super(dependencies, options) - const messageSchemas = options.messageSchemas - this.messageSchemaContainer = new MessageSchemaContainer({ - messageSchemas, - messageTypeField: options.messageTypeField, - }) + this.messageSchemaContainer = this.resolvePublisherMessageSchemaContainer(options) } async publish(message: MessagePayloadType, options: SNSMessageOptions = {}): Promise { diff --git a/packages/sns/lib/sns/SnsPublisherManager.spec.ts b/packages/sns/lib/sns/SnsPublisherManager.spec.ts index ff443459..9b2e607c 100644 --- a/packages/sns/lib/sns/SnsPublisherManager.spec.ts +++ b/packages/sns/lib/sns/SnsPublisherManager.spec.ts @@ -4,6 +4,7 @@ import { enrichMessageSchemaWithBase } from '@message-queue-toolkit/core' import type { AwilixContainer } from 'awilix' import z from 'zod' +import { SnsSqsEntityConsumer } from '../../test/consumers/SnsSqsEntityConsumer' import type { Dependencies, TestEventPublishPayloadsType, @@ -13,7 +14,6 @@ import { registerDependencies, TestEvents } from '../../test/utils/testContext' import { CommonSnsPublisher } from './CommonSnsPublisherFactory' import type { SnsPublisherManager } from './SnsPublisherManager' -import { FakeConsumer } from './fakes/FakeConsumer' describe('SnsPublisherManager', () => { let diContainer: AwilixContainer @@ -23,7 +23,7 @@ describe('SnsPublisherManager', () => { > beforeAll(async () => { - diContainer = await registerDependencies() + diContainer = await registerDependencies({}, false) publisherManager = diContainer.cradle.publisherManager }) @@ -34,13 +34,8 @@ describe('SnsPublisherManager', () => { describe('publish', () => { it('publishes to a correct publisher', async () => { // Given - const fakeConsumer = new FakeConsumer( - diContainer.cradle, - 'queue', - TestEvents.created.snsTopic, - TestEvents.created.consumerSchema, - ) - await fakeConsumer.start() + const consumer = new SnsSqsEntityConsumer(diContainer.cradle) + await consumer.start() // When const publishedMessage = await publisherManager.publish(TestEvents.created.snsTopic, { @@ -54,7 +49,7 @@ describe('SnsPublisherManager', () => { .handlerSpy(TestEvents.created.snsTopic) .waitForMessageWithId(publishedMessage.id) - const consumerResult = await fakeConsumer.handlerSpy.waitForMessageWithId(publishedMessage.id) + const consumerResult = await consumer.handlerSpy.waitForMessageWithId(publishedMessage.id) const publishedMessageResult = await handlerSpyPromise expect(consumerResult.processingResult).toBe('consumed') @@ -75,7 +70,7 @@ describe('SnsPublisherManager', () => { type: 'entity.created', }) - await fakeConsumer.close() + await consumer.close() }) it('message publishing is type-safe', async () => { diff --git a/packages/sns/test/consumers/SnsSqsEntityConsumer.ts b/packages/sns/test/consumers/SnsSqsEntityConsumer.ts new file mode 100644 index 00000000..65c24405 --- /dev/null +++ b/packages/sns/test/consumers/SnsSqsEntityConsumer.ts @@ -0,0 +1,100 @@ +import { MessageHandlerConfigBuilder } from '@message-queue-toolkit/core' + +import type { + SNSSQSConsumerDependencies, + SNSSQSConsumerOptions, +} from '../../lib/sns/AbstractSnsSqsConsumer' +import { AbstractSnsSqsConsumer } from '../../lib/sns/AbstractSnsSqsConsumer' +import type { TestEventConsumerPayloadsType } from '../utils/testContext' +import { TestEvents } from '../utils/testContext' + +import { entityCreatedHandler } from './handlers/EntityCreatedHandler' +import { entityUpdatedHandler } from './handlers/EntityUpdatedHandler' + +type SupportedMessages = TestEventConsumerPayloadsType +type ExecutionContext = { + incrementAmount: number +} +type PreHandlerOutput = { + preHandlerCount: number +} + +type SnsSqsPermissionConsumerOptions = Pick< + SNSSQSConsumerOptions, + | 'creationConfig' + | 'locatorConfig' + | 'deletionConfig' + | 'deadLetterQueue' + | 'consumerOverrides' + | 'maxRetryDuration' +> + +export class SnsSqsEntityConsumer extends AbstractSnsSqsConsumer< + SupportedMessages, + ExecutionContext, + PreHandlerOutput +> { + public static readonly CONSUMED_QUEUE_NAME = 'entities_queue' + public static readonly SUBSCRIBED_TOPIC_NAME = 'dummy' + + constructor( + dependencies: SNSSQSConsumerDependencies, + options: SnsSqsPermissionConsumerOptions = { + creationConfig: { + queue: { + QueueName: SnsSqsEntityConsumer.CONSUMED_QUEUE_NAME, + }, + topic: { + Name: SnsSqsEntityConsumer.SUBSCRIBED_TOPIC_NAME, + }, + }, + }, + ) { + super( + dependencies, + { + handlerSpy: true, + handlers: new MessageHandlerConfigBuilder< + SupportedMessages, + ExecutionContext, + PreHandlerOutput + >() + .addConfig(TestEvents.created, entityCreatedHandler, {}) + .addConfig(TestEvents.updated, entityUpdatedHandler, {}) + .build(), + deletionConfig: options.deletionConfig ?? { + deleteIfExists: true, + }, + consumerOverrides: options.consumerOverrides ?? { + terminateVisibilityTimeout: true, // this allows to retry failed messages immediately + }, + ...(options.locatorConfig + ? { locatorConfig: options.locatorConfig } + : { + creationConfig: options.creationConfig ?? { + queue: { QueueName: SnsSqsEntityConsumer.CONSUMED_QUEUE_NAME }, + topic: { Name: SnsSqsEntityConsumer.SUBSCRIBED_TOPIC_NAME }, + }, + }), + messageTypeField: 'type', + subscriptionConfig: { + updateAttributesIfExists: false, + }, + maxRetryDuration: options.maxRetryDuration, + }, + { + incrementAmount: 1, + }, + ) + } + + get subscriptionProps() { + return { + topicArn: this.topicArn, + queueUrl: this.queueUrl, + queueName: this.queueName, + subscriptionArn: this.subscriptionArn, + deadLetterQueueUrl: this.deadLetterQueueUrl, + } + } +} diff --git a/packages/sns/test/consumers/handlers/EntityCreatedHandler.ts b/packages/sns/test/consumers/handlers/EntityCreatedHandler.ts new file mode 100644 index 00000000..cd5509a4 --- /dev/null +++ b/packages/sns/test/consumers/handlers/EntityCreatedHandler.ts @@ -0,0 +1,14 @@ +import type { Either } from '@lokalise/node-core' +import type z from 'zod' + +import type { TestEvents } from '../../utils/testContext' + +let _latestData: string + +export async function entityCreatedHandler( + message: z.infer, +): Promise> { + _latestData = message.payload.newData + + return { result: 'success' } +} diff --git a/packages/sns/test/consumers/handlers/EntityUpdatedHandler.ts b/packages/sns/test/consumers/handlers/EntityUpdatedHandler.ts new file mode 100644 index 00000000..3dce5d2d --- /dev/null +++ b/packages/sns/test/consumers/handlers/EntityUpdatedHandler.ts @@ -0,0 +1,14 @@ +import type { Either } from '@lokalise/node-core' +import type z from 'zod' + +import type { TestEvents } from '../../utils/testContext' + +let _latestData: string + +export async function entityUpdatedHandler( + message: z.infer, +): Promise> { + _latestData = message.payload.updatedData + + return { result: 'success' } +} diff --git a/packages/sns/test/utils/testContext.ts b/packages/sns/test/utils/testContext.ts index bb29e33e..f91ca452 100644 --- a/packages/sns/test/utils/testContext.ts +++ b/packages/sns/test/utils/testContext.ts @@ -54,6 +54,7 @@ export const TestEvents = { export type TestEventsType = (typeof TestEvents)[keyof typeof TestEvents][] export type TestEventPublishPayloadsType = z.infer +export type TestEventConsumerPayloadsType = z.infer export async function registerDependencies( dependencyOverrides: DependencyOverrides = {}, diff --git a/packages/sqs/lib/sqs/AbstractSqsConsumer.ts b/packages/sqs/lib/sqs/AbstractSqsConsumer.ts index 838e4636..d9c0db59 100644 --- a/packages/sqs/lib/sqs/AbstractSqsConsumer.ts +++ b/packages/sqs/lib/sqs/AbstractSqsConsumer.ts @@ -10,13 +10,9 @@ import type { QueueConsumerDependencies, DeadLetterQueueOptions, ParseMessageResult, -} from '@message-queue-toolkit/core' -import { - isMessageError, - parseMessage, - HandlerContainer, MessageSchemaContainer, } from '@message-queue-toolkit/core' +import { isMessageError, parseMessage, HandlerContainer } from '@message-queue-toolkit/core' import { Consumer } from 'sqs-consumer' import type { ConsumerOptions } from 'sqs-consumer/src/types' @@ -115,9 +111,10 @@ export abstract class AbstractSqsConsumer< protected deadLetterQueueUrl?: string protected readonly errorResolver: ErrorResolver - protected readonly messageSchemaContainer: MessageSchemaContainer protected readonly executionContext: ExecutionContext + public readonly messageSchemaContainer: MessageSchemaContainer + protected constructor( dependencies: SQSConsumerDependencies, options: ConsumerOptionsType, @@ -131,11 +128,7 @@ export abstract class AbstractSqsConsumer< this.maxRetryDuration = options.maxRetryDuration ?? DEFAULT_MAX_RETRY_DURATION this.executionContext = executionContext - const messageSchemas = options.handlers.map((entry) => entry.schema) - this.messageSchemaContainer = new MessageSchemaContainer({ - messageSchemas, - messageTypeField: options.messageTypeField, - }) + this.messageSchemaContainer = this.resolveConsumerMessageSchemaContainer(options) this.handlerContainer = new HandlerContainer< MessagePayloadType, ExecutionContext, diff --git a/packages/sqs/lib/sqs/AbstractSqsPublisher.ts b/packages/sqs/lib/sqs/AbstractSqsPublisher.ts index ef3aa2da..1143af6a 100644 --- a/packages/sqs/lib/sqs/AbstractSqsPublisher.ts +++ b/packages/sqs/lib/sqs/AbstractSqsPublisher.ts @@ -8,8 +8,8 @@ import type { MessageValidationError, BarrierResult, QueuePublisherOptions, + MessageSchemaContainer, } from '@message-queue-toolkit/core' -import { MessageSchemaContainer } from '@message-queue-toolkit/core' import type { ZodSchema } from 'zod' import type { SQSMessage } from '../types/MessageTypes' @@ -35,11 +35,7 @@ export abstract class AbstractSqsPublisher ) { super(dependencies, options) - const messageSchemas = options.messageSchemas - this.messageSchemaContainer = new MessageSchemaContainer({ - messageSchemas, - messageTypeField: options.messageTypeField, - }) + this.messageSchemaContainer = this.resolvePublisherMessageSchemaContainer(options) } async publish(message: MessagePayloadType, options: SQSMessageOptions = {}): Promise {