UNPKG

@solace-community/angular-solace-message-client

Version:

Message client to communicate with a Solace messaging broker for sending and receiving messages using the native SMF protocol (Solace Message Format) over web socket. The library is designed to be used in Angular applications.

1 lines 120 kB
{"version":3,"file":"solace-community-angular-solace-message-client.mjs","sources":["../../../../projects/solace-message-client/src/lib/logger.ts","../../../../projects/solace-message-client/src/lib/solace-session-provider.ts","../../../../projects/solace-message-client/src/lib/solace-message-client.config.ts","../../../../projects/solace-message-client/src/lib/topic-matcher.ts","../../../../projects/solace-message-client/src/lib/topic-subscription-counter.ts","../../../../projects/solace-message-client/src/lib/serial-executor.service.ts","../../../../projects/solace-message-client/src/lib/ɵdestroy-ref.ts","../../../../projects/solace-message-client/src/lib/ɵsolace-message-client.ts","../../../../projects/solace-message-client/src/lib/solace-message-client.ts","../../../../projects/solace-message-client/src/lib/solace-message-client.provider.ts","../../../../projects/solace-message-client/src/solace-community-angular-solace-message-client.ts"],"sourcesContent":["import {EnvironmentProviders, inject, Injectable, makeEnvironmentProviders, Type} from '@angular/core';\nimport {LogLevel} from 'solclientjs';\n\n/**\n * Logger used by the Angular Solace Message Client.\n *\n * The default log level can be changed as follows:\n * - Change the log level programmatically by providing it under the DI token {@link LogLevel}:\n * `{provide: LogLevel, useValue: LogLevel.DEBUG}`\n * - Change the log level at runtime via session storage by adding the following entry and then reloading the application:\n * key: `angular-solace-message-client#loglevel`\n * value: `debug` // supported values are: trace | debug | info | warn | error | fatal\n */\n@Injectable()\nexport class Logger {\n\n public readonly logLevel = this.readLogLevelFromSessionStorage(inject(LogLevel as unknown as Type<LogLevel>));\n\n public debug(...data: any[]): void {\n if (this.logLevel >= LogLevel.DEBUG) {\n console.debug(...this.addLogPrefix(data));\n }\n }\n\n public info(...data: any[]): void {\n if (this.logLevel >= LogLevel.INFO) {\n console.info(...this.addLogPrefix(data));\n }\n }\n\n public warn(...data: any[]): void {\n if (this.logLevel >= LogLevel.WARN) {\n console.warn(...this.addLogPrefix(data));\n }\n }\n\n public error(...data: any[]): void {\n if (this.logLevel >= LogLevel.ERROR) {\n console.error(...this.addLogPrefix(data));\n }\n }\n\n private addLogPrefix(args: unknown[]): unknown[] {\n return [`[SolaceMessageClient] ${args[0]}`, ...args.slice(1)];\n }\n\n private readLogLevelFromSessionStorage(defaultLogLevel: LogLevel): LogLevel {\n const level = sessionStorage.getItem('angular-solace-message-client#loglevel');\n switch (level) {\n case 'trace':\n return LogLevel.TRACE;\n case 'debug':\n return LogLevel.DEBUG;\n case 'info':\n return LogLevel.INFO;\n case 'warn':\n return LogLevel.WARN;\n case 'error':\n return LogLevel.ERROR;\n case 'fatal':\n return LogLevel.FATAL;\n default:\n return defaultLogLevel;\n }\n }\n}\n\n/**\n * Registers a set of DI providers to enable logging in the Angular Solace Message Client.\n */\nexport function provideLogger(logLevel: LogLevel): EnvironmentProviders {\n return makeEnvironmentProviders([\n Logger,\n // Provide inherited 'LogLevel' or provide default LogLevel otherwise.\n {provide: LogLevel, useFactory: () => inject(LogLevel as unknown as Type<LogLevel>, {skipSelf: true, optional: true}) ?? logLevel},\n ]);\n}\n","import {inject, Injectable} from '@angular/core';\nimport {Session, SessionProperties, SolclientFactory, SolclientFactoryProfiles, SolclientFactoryProperties} from 'solclientjs';\nimport {Logger} from './logger';\n\n/**\n * Creates a {@link Session} from a given config to connect to the Solace message broker.\n *\n * You may override the default session provider in order to customize session construction, e.g., in tests, as following:\n * `{provide: SolaceSessionProvider, useClass: YourSessionProvider}`\n *\n * The default provider does the following: `solace.SolclientFactory.createSession(sessionProperties)`.\n */\n@Injectable()\nexport abstract class SolaceSessionProvider {\n\n /**\n * Method invoked to create a {@link Session} from given properties.\n */\n public abstract provide(sessionProperties: SessionProperties): Session;\n}\n\n/**\n * @docs-private\n */\n@Injectable()\nexport class ɵSolaceSessionProvider implements SolaceSessionProvider {\n\n constructor() {\n const factoryProperties = new SolclientFactoryProperties();\n factoryProperties.profile = SolclientFactoryProfiles.version10_5;\n factoryProperties.logLevel = inject(Logger).logLevel;\n SolclientFactory.init(factoryProperties);\n }\n\n public provide(sessionProperties: SessionProperties): Session {\n return SolclientFactory.createSession(sessionProperties);\n }\n}\n","import {SessionProperties} from 'solclientjs';\nimport {InjectionToken} from '@angular/core';\nimport {OAuthAccessTokenFn} from './oauth-access-token-provider';\nimport {Observable} from 'rxjs';\n\n/**\n * DI token to inject the config of Angular Solace Message Client.\n */\nexport const SOLACE_MESSAGE_CLIENT_CONFIG = new InjectionToken<SolaceMessageClientConfig | SolaceMessageClientConfigFn>('SOLACE_MESSAGE_CLIENT_CONFIG');\n\n/**\n * Configures the {@link SolaceMessageClient} to connect to the Solace message broker.\n */\nexport interface SolaceMessageClientConfig extends Omit<SessionProperties, 'accessToken'> {\n /**\n * Specifies the access token for OAuth 2.0 authentication.\n *\n * Property is ignored if not using OAuth 2.0 authentication scheme.\n *\n * @see {@link OAuthAccessTokenFn}\n */\n accessToken?: string | OAuthAccessTokenFn;\n}\n\n/**\n * Signature of a function to load the config of Angular Solace Message Client.\n *\n * The function can call `inject` to get any required dependencies.\n */\nexport type SolaceMessageClientConfigFn = () => SolaceMessageClientConfig | Promise<SolaceMessageClientConfig> | Observable<SolaceMessageClientConfig>;\n","/**\n * Matches exact topics as used when publishing messages against subscription topics.\n *\n * This class implements the rules for 'Wildcard Characters in SMF Topic Subscriptions',\n * as outlined here: https://docs.solace.com/PubSub-Basics/Wildcard-Charaters-Topic-Subs.htm.\n */\nexport class TopicMatcher {\n\n private readonly _subscriptionTopic: string[];\n\n constructor(subscriptionTopic: string) {\n this._subscriptionTopic = parseSubscriptionTopic(subscriptionTopic);\n }\n\n public matches(topic: string | undefined): boolean {\n if (!topic) {\n return false;\n }\n const testeeSegments = topic.split('/');\n const subscriptionSegments = this._subscriptionTopic;\n\n for (let i = 0; i < subscriptionSegments.length; i++) {\n const subscriptionTopicSegment = subscriptionSegments[i]!;\n const testee = testeeSegments[i];\n const isLastSubscriptionTopicSegment = (i === subscriptionSegments.length - 1);\n\n if (testee === undefined) {\n return false;\n }\n\n if (subscriptionTopicSegment === '>' && isLastSubscriptionTopicSegment) {\n return true;\n }\n\n if (subscriptionTopicSegment === '*') {\n continue;\n }\n\n if (subscriptionTopicSegment.endsWith('*') && testee.startsWith(subscriptionTopicSegment.slice(0, -1))) {\n continue;\n }\n\n if (subscriptionTopicSegment !== testee) {\n return false;\n }\n }\n return testeeSegments.length === subscriptionSegments.length;\n }\n}\n\n/**\n * Parses the subscription topic, removing #noexport and #share segments, if any.\n */\nfunction parseSubscriptionTopic(topic: string): string[] {\n const segments = topic.split('/');\n\n // Remove #noexport segment, if any. See https://docs.solace.com/Messaging/No-Export.htm\n // Example: #noexport/#share/ShareName/topicFilter, #noexport/topicFilter\n if (segments[0] === '#noexport') {\n segments.shift();\n }\n\n // Remove #share segments, if any. See https://docs.solace.com/Messaging/Direct-Msg/Direct-Messages.htm\n // Examples: #share/<ShareName>/<topicFilter>\n if (segments[0] === '#share') {\n segments.shift(); // removes #share segment\n segments.shift(); // removes share name segment\n }\n return segments;\n}\n","import {Destination} from 'solclientjs';\n\n/**\n * Maintains the subscription count per topic.\n */\nexport class TopicSubscriptionCounter {\n\n private _subscriptionCounts = new Map<string, number>();\n\n public incrementAndGet(topic: string | Destination): number {\n const topicName = coerceTopicName(topic);\n const count = (this._subscriptionCounts.get(topicName) ?? 0) + 1;\n this._subscriptionCounts.set(topicName, count);\n return count;\n }\n\n public decrementAndGet(topic: string | Destination): number {\n const topicName = coerceTopicName(topic);\n const count = Math.max(0, (this._subscriptionCounts.get(topicName) ?? 0) - 1);\n if (count === 0) {\n this._subscriptionCounts.delete(topicName);\n }\n else {\n this._subscriptionCounts.set(topicName, count);\n }\n return count;\n }\n\n public destroy(): void {\n this._subscriptionCounts.clear();\n }\n}\n\nfunction coerceTopicName(topic: string | Destination): string {\n if (typeof topic === 'string') {\n return topic;\n }\n return topic.getName();\n}\n","import {Subject} from 'rxjs';\nimport {catchError, mergeMap, takeUntil} from 'rxjs/operators';\nimport {Logger} from './logger';\n\nexport type Task<T> = () => Promise<T>;\n\n/**\n * Allows the mutual exclusive execution of tasks.\n *\n * Scheduled tasks are executed sequentially, one at a time, until all tasks have executed.\n * A task completed execution when its Promise resolved or rejected.\n */\nexport class SerialExecutor {\n\n private _destroy$ = new Subject<void>();\n private _tasks$ = new Subject<Task<any>>();\n\n constructor(logger: Logger) {\n this._tasks$\n .pipe(\n mergeMap(task => task(), 1), // serialize execution\n catchError((error, caught) => {\n logger.error('Unexpected: ', error);\n return caught;\n }),\n takeUntil(this._destroy$),\n )\n .subscribe();\n }\n\n /**\n * Schedules the given task for serial execution.\n * The task will be executed after all previously scheduled tasks have finished execution.\n *\n * @return Promise that resolves to the task's return value when it finished execution, or\n * that rejects when the taks's Promise rejects.\n */\n public scheduleSerial<T>(task: Task<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n this._tasks$.next(() => task().then(resolve).catch(reject));\n });\n }\n\n public destroy(): void {\n this._destroy$.next();\n }\n}\n","/*\n * Copyright (c) 2018-2023 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {DestroyRef, inject} from '@angular/core';\nimport {noop} from 'rxjs';\n\n/**\n * Implementation of {@link DestroyRef} that can be manually disposed.\n *\n * Binds to the {@link DestroyRef} of the current injection context if constructed inside an injection context.\n */\nexport class ɵDestroyRef implements DestroyRef {\n\n private _callbacks = new Set<() => void>();\n private _destroyed = false;\n\n constructor() {\n inject(DestroyRef, {optional: true})?.onDestroy(() => this.destroy());\n }\n\n public onDestroy(callback: () => void): () => void {\n if (this._destroyed) {\n callback();\n return noop;\n }\n\n this._callbacks.add(callback);\n return () => this._callbacks.delete(callback);\n }\n\n public destroy(): void {\n this._callbacks.forEach(callback => callback());\n this._callbacks.clear();\n this._destroyed = true;\n }\n\n public get destroyed(): boolean {\n return this._destroyed;\n }\n}\n","import {assertInInjectionContext, inject, Injectable, Injector, NgZone, runInInjectionContext} from '@angular/core';\nimport {EMPTY, EmptyError, firstValueFrom, merge, MonoTypeOperatorFunction, noop, Observable, Observer, of, OperatorFunction, ReplaySubject, share, shareReplay, skip, Subject, TeardownLogic, throwError} from 'rxjs';\nimport {distinctUntilChanged, filter, finalize, map, mergeMap, take, takeUntil, tap} from 'rxjs/operators';\nimport {UUID} from '@scion/toolkit/uuid';\nimport {ConsumeOptions, Data, MessageEnvelope, ObserveOptions, PublishOptions, RequestOptions, SolaceMessageClient} from './solace-message-client';\nimport {TopicMatcher} from './topic-matcher';\nimport {observeIn, subscribeIn} from '@scion/toolkit/operators';\nimport {SolaceSessionProvider} from './solace-session-provider';\nimport {SOLACE_MESSAGE_CLIENT_CONFIG, SolaceMessageClientConfig} from './solace-message-client.config';\nimport {AuthenticationScheme, Destination, Message, MessageConsumer, MessageConsumerEventName, MessageConsumerProperties, MessageDeliveryModeType, OperationError, QueueBrowser, QueueBrowserEventName, QueueBrowserProperties, QueueDescriptor, QueueType, RequestError, SDTField, SDTFieldType, SDTMapContainer, Session, SessionEvent, SessionEventCode, SessionProperties as SolaceSessionProperties, SolclientFactory} from 'solclientjs';\nimport {TopicSubscriptionCounter} from './topic-subscription-counter';\nimport {SerialExecutor} from './serial-executor.service';\nimport {Logger} from './logger';\nimport {takeUntilDestroyed} from '@angular/core/rxjs-interop';\nimport {Observables} from '@scion/toolkit/util';\nimport {ɵDestroyRef} from './ɵdestroy-ref';\n\n@Injectable()\nexport class ɵSolaceMessageClient implements SolaceMessageClient {\n\n public readonly connected$: Observable<boolean>;\n\n private readonly _sessionProvider = inject(SolaceSessionProvider);\n private readonly _injector = inject(Injector);\n private readonly _logger = inject(Logger);\n private readonly _zone = inject(NgZone);\n private readonly _destroyRef = new ɵDestroyRef();\n\n private readonly _message$ = new Subject<Message>();\n private readonly _event$ = new Subject<SessionEvent>();\n\n private _session: Promise<Session> | null = null;\n private _subscriptionExecutor: SerialExecutor | null = null;\n private _subscriptionCounter: TopicSubscriptionCounter | null = null;\n\n constructor() {\n this.disposeWhenSolaceSessionDied();\n this.logSolaceSessionEvents();\n this.connected$ = this.monitorConnectionState$();\n\n // Connect to the Solace Message Broker.\n this._session = this.loadConfig()\n .then(config => {\n this._logger.debug('Connecting to Solace message broker:', obfuscateSecrets(config));\n return this.createSession(config);\n })\n .catch((error: unknown) => {\n this._logger.error('Failed to connect to the Solace message broker.', error);\n return Promise.reject(error as Error);\n });\n this._destroyRef.onDestroy(() => void this.dispose());\n }\n\n /**\n * Creates a Solace session based on the passed configuration.\n *\n * @return Promise that resolves to the {@link Session} when connected to the broker, or that rejects if the connection attempt failed.\n */\n private createSession(config: SolaceMessageClientConfig): Promise<Session> {\n return new Promise<Session>((resolve, reject) => void this._zone.runOutsideAngular(async () => {\n this._subscriptionExecutor = new SerialExecutor(this._logger);\n this._subscriptionCounter = new TopicSubscriptionCounter();\n\n // If using OAUTH2 authentication scheme, create access token Observable to continuously inject a valid access token into the Solace session.\n const oAuth2Scheme = config.authenticationScheme === AuthenticationScheme.OAUTH2;\n const accessToken$: Observable<string> | null = oAuth2Scheme ? provideOAuthAccessToken$(config, {injector: this._injector}).pipe(shareReplay({bufferSize: 1, refCount: false})) : null;\n\n // If using OAUTH2 authentication scheme, set the initial access token via session properties.\n if (accessToken$) {\n config.accessToken = await firstValueFrom(accessToken$.pipe(subscribeIn(fn => this._zone.run(fn)))).catch(mapEmptyError(() => Error('[EmptyAccessTokenError] Access token Observable has completed without emitted an access token.')));\n }\n\n // Create the Solace session.\n const session: Session = this._sessionProvider.provide(new SolaceSessionProperties(config));\n\n // Provide the session with a valid \"OAuth 2.0 Access Token\".\n // The Observable is expected to never complete and continuously emit a renewed token short before expiration of the previously emitted token.\n accessToken$?.pipe(\n skip(1), // initial access token is set via session properties\n takeUntilDestroyed(this._destroyRef),\n ).subscribe({\n next: accessToken => {\n this._logger.debug('Injecting \"OAuth 2.0 Access Token\" into Solace session.', accessToken);\n session.updateAuthenticationOnReconnect({accessToken});\n },\n complete: () => {\n if (this._session) {\n this._logger.warn('[AccessTokenProviderCompletedWarning] Observable providing access token(s) to the Solace session has completed. The Observable should NEVER complete and continuously emit the renewed token short before expiration of the previously emitted token. Otherwise, the connection to the broker would not be re-established in the event of a network interruption.');\n }\n },\n error: error => {\n this._logger.error(error);\n },\n });\n\n // When the Session is ready to send/receive messages and perform control operations.\n session.on(SessionEventCode.UP_NOTICE, (event: SessionEvent) => {\n this._event$.next(event);\n this._logger.debug('Connected to Solace message broker.', obfuscateSecrets(config));\n resolve(session);\n });\n\n // When the session has gone down, and an automatic reconnection attempt is in progress.\n session.on(SessionEventCode.RECONNECTED_NOTICE, (event: SessionEvent) => this._event$.next(event));\n\n // Emits when the session was established and then went down.\n session.on(SessionEventCode.DOWN_ERROR, (event: SessionEvent) => this._event$.next(event));\n\n // Emits when the session attempted to connect but was unsuccessful.\n session.on(SessionEventCode.CONNECT_FAILED_ERROR, (event: SessionEvent) => {\n this._event$.next(event);\n reject(event); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors\n });\n\n // When the session connect operation failed, or the session that was once up, is now disconnected.\n session.on(SessionEventCode.DISCONNECTED, (event: SessionEvent) => this._event$.next(event));\n\n // When the session has gone down, and an automatic reconnection attempt is in progress.\n session.on(SessionEventCode.RECONNECTING_NOTICE, (event: SessionEvent) => this._event$.next(event));\n\n // When a direct message was received on the session.\n session.on(SessionEventCode.MESSAGE, (message: Message): void => this._message$.next(message));\n\n // When a subscribe or unsubscribe operation succeeded.\n session.on(SessionEventCode.SUBSCRIPTION_OK, (event: SessionEvent) => this._event$.next(event));\n\n // When a subscribe or unsubscribe operation was rejected by the broker.\n session.on(SessionEventCode.SUBSCRIPTION_ERROR, (event: SessionEvent) => this._event$.next(event));\n\n // When a message published with a guaranteed message delivery strategy, that is {@link MessageDeliveryModeType.PERSISTENT} or {@link MessageDeliveryModeType.NON_PERSISTENT}, was acknowledged by the router.\n session.on(SessionEventCode.ACKNOWLEDGED_MESSAGE, (event: SessionEvent) => this._event$.next(event));\n\n // When a message published with a guaranteed message delivery strategy, that is {@link MessageDeliveryModeType.PERSISTENT} or {@link MessageDeliveryModeType.NON_PERSISTENT}, was rejected by the router.\n session.on(SessionEventCode.REJECTED_MESSAGE_ERROR, (event: SessionEvent) => this._event$.next(event));\n\n session.connect();\n }).catch((error: unknown) => reject(error as Error)));\n }\n\n /**\n * Disposes the Solace session, disconnecting this client from the Solace message broker. Has no effect if already disposed.\n *\n * Messages cannot be received or published after disposing the session.\n *\n * @param options - Controls disposal of the Solace session.\n * @param options.force - Controls if to first disconnect from the broker before destroying the Solace session,\n * enabling graceful shutdown of the connnection for the broker to clean up resources.\n * Defaults to `false`.\n * @return A Promise that resolves when disposed the session.\n */\n public async dispose(options?: {force?: true}): Promise<void> {\n const session = await this._session?.catch(noop); // do not error on dispose\n if (!session) {\n return;\n }\n\n this._session = null;\n\n // Disconnect from broker.\n const force = options?.force ?? false;\n if (!force) {\n try {\n const whenDisconnected = this.whenEvent(SessionEventCode.DISCONNECTED);\n session.disconnect();\n await whenDisconnected;\n }\n catch (error) {\n this._logger.warn('Failed to gracefully disconnect from the Solace Message Broker. Disposing the Solace session.', error);\n }\n }\n\n // Dispose session.\n try {\n session.dispose();\n }\n finally {\n this._subscriptionExecutor?.destroy();\n this._subscriptionExecutor = null;\n\n this._subscriptionCounter?.destroy();\n this._subscriptionCounter = null;\n\n this._destroyRef.destroy();\n }\n }\n\n public observe$(topic: string, options?: ObserveOptions): Observable<MessageEnvelope> {\n return new Observable((observer: Observer<MessageEnvelope>): TeardownLogic => {\n const unsubscribe$ = new Subject<void>();\n const topicDestination = createSubscriptionTopicDestination(topic);\n const topicMatcher = new TopicMatcher(topicDestination.getName());\n\n // Wait until initialized the session so that 'subscriptionExecutor' and 'subscriptionCounter' are initialized.\n this.session\n .then(() => {\n const subscribeError$ = new Subject<never>();\n let subscriptionErrored = false;\n\n // Filter messages sent to the given topic.\n merge(this._message$, subscribeError$)\n .pipe(\n assertNotInAngularZone(),\n filter(message => topicMatcher.matches(message.getDestination()?.getName())),\n mapToMessageEnvelope(topic),\n synchronizeNgZone(this._zone),\n takeUntilDestroyed(this._destroyRef),\n takeUntil(unsubscribe$),\n finalize(() => {\n // Unsubscribe from the topic on the Solace session, but only if being the last subscription on that topic and if successfully subscribed to the Solace broker.\n if (this._subscriptionCounter?.decrementAndGet(topicDestination) === 0 && !subscriptionErrored) {\n void this.unsubscribeFromTopic(topicDestination);\n }\n }),\n )\n .subscribe(observer);\n\n // Subscribe to the topic on the Solace session, but only if being the first subscription on that topic.\n if (this._subscriptionCounter!.incrementAndGet(topicDestination) === 1) {\n void this.subscribeToTopic(topicDestination, options).then(success => {\n if (success) {\n options?.onSubscribed?.();\n }\n else {\n subscriptionErrored = true;\n subscribeError$.error(`Failed to subscribe to topic ${topicDestination.getName()}.`);\n }\n });\n }\n else {\n options?.onSubscribed?.();\n }\n })\n .catch((error: unknown) => {\n observer.error(error);\n });\n\n return (): void => unsubscribe$.next();\n });\n }\n\n /**\n * Subscribes to the given topic on the Solace session.\n */\n private subscribeToTopic(topic: Destination, observeOptions?: ObserveOptions): Promise<boolean> {\n // Calls to `solace.Session.subscribe` and `solace.Session.unsubscribe` must be executed one after the other until the Solace Broker confirms\n // the operation. Otherwise a previous unsubscribe may cancel a later subscribe on the same topic.\n return this._subscriptionExecutor!.scheduleSerial(async () => {\n try {\n // IMPORTANT: Do not subscribe when the session is down, that is, after received a DOWN_ERROR. Otherwise, solclientjs would crash.\n // When the session is down, the session Promise resolves to `null`.\n const session = await this._session;\n if (!session) {\n return false;\n }\n\n const subscribeCorrelationKey = UUID.randomUUID();\n const whenSubscribed = this.whenEvent(SessionEventCode.SUBSCRIPTION_OK, {rejectOnEvent: SessionEventCode.SUBSCRIPTION_ERROR, correlationKey: subscribeCorrelationKey})\n .then(() => true)\n .catch((event: unknown) => {\n this._logger.warn(`Solace event broker rejected subscription on topic ${topic.getName()}.`, event);\n return false;\n });\n\n session.subscribe(\n topic,\n true,\n subscribeCorrelationKey,\n observeOptions?.subscribeTimeout,\n );\n return await whenSubscribed;\n }\n catch {\n return false;\n }\n });\n }\n\n /**\n * Unsubscribes from the given topic on the Solace session.\n */\n private unsubscribeFromTopic(topic: Destination): Promise<boolean> {\n // Calls to `solace.Session.subscribe` and `solace.Session.unsubscribe` must be executed one after the other until the Solace Broker confirms\n // the operation. Otherwise a previous unsubscribe may cancel a later subscribe on the same topic.\n return this._subscriptionExecutor!.scheduleSerial(async () => {\n try {\n // IMPORTANT: Do not unsubscribe when the session is down, that is, after received a DOWN_ERROR. Otherwise, solclientjs would crash.\n // When the session is down, the session Promise resolves to `null`.\n const session = await this._session;\n if (!session) {\n return false;\n }\n\n const unsubscribeCorrelationKey = UUID.randomUUID();\n const whenUnsubscribed = this.whenEvent(SessionEventCode.SUBSCRIPTION_OK, {rejectOnEvent: SessionEventCode.SUBSCRIPTION_ERROR, correlationKey: unsubscribeCorrelationKey})\n .then(() => true)\n .catch((error: unknown) => {\n this._logger.warn(`Solace event broker rejected unsubscription on topic ${topic.getName()}.`, error);\n return false;\n });\n\n session.unsubscribe(\n topic,\n true,\n unsubscribeCorrelationKey,\n undefined,\n );\n return await whenUnsubscribed;\n }\n catch {\n return false;\n }\n });\n }\n\n public consume$(topicOrDescriptor: string | (MessageConsumerProperties & ConsumeOptions)): Observable<MessageEnvelope> {\n // If passed a `string` literal, subscribe to a non-durable topic endpoint.\n if (typeof topicOrDescriptor === 'string') {\n return this.createMessageConsumer$({\n topicEndpointSubscription: SolclientFactory.createTopicDestination(topicOrDescriptor),\n queueDescriptor: new QueueDescriptor({type: QueueType.TOPIC_ENDPOINT, durable: false}),\n });\n }\n\n return this.createMessageConsumer$(topicOrDescriptor);\n }\n\n private createMessageConsumer$(consumerProperties: MessageConsumerProperties & ConsumeOptions): Observable<MessageEnvelope> {\n const topicEndpointSubscription = consumerProperties.topicEndpointSubscription?.getName();\n if (topicEndpointSubscription) {\n consumerProperties.topicEndpointSubscription = createSubscriptionTopicDestination(consumerProperties.topicEndpointSubscription!.getName());\n }\n\n return new Observable((observer: Observer<Message>): TeardownLogic => {\n let messageConsumer: MessageConsumer | undefined;\n this.session\n .then(session => {\n messageConsumer = session.createMessageConsumer(consumerProperties);\n\n // Define message consumer event listeners\n messageConsumer.on(MessageConsumerEventName.UP, () => {\n this._logger.debug(`MessageConsumerEvent: UP`);\n consumerProperties.onSubscribed?.(messageConsumer!);\n });\n messageConsumer.on(MessageConsumerEventName.CONNECT_FAILED_ERROR, (error: OperationError) => {\n this._logger.debug(`MessageConsumerEvent: CONNECT_FAILED_ERROR`, error);\n observer.error(error);\n });\n messageConsumer.on(MessageConsumerEventName.DOWN_ERROR, (error: OperationError) => {\n this._logger.debug(`MessageConsumerEvent: DOWN_ERROR`, error);\n observer.error(error);\n });\n messageConsumer.on(MessageConsumerEventName.DOWN, () => { // event emitted after successful disconnect request\n this._logger.debug(`MessageConsumerEvent: DOWN`);\n messageConsumer?.dispose();\n observer.complete();\n });\n\n // Define message event listener\n messageConsumer.on(MessageConsumerEventName.MESSAGE, (message: Message) => {\n this._logger.debug(`MessageConsumerEvent: MESSAGE`, message);\n NgZone.assertNotInAngularZone();\n observer.next(message);\n });\n\n // Connect the message consumer\n messageConsumer.connect();\n })\n .catch((error: unknown) => {\n observer.error(error);\n messageConsumer?.dispose();\n });\n\n return (): void => {\n // Initiate an orderly disconnection of the consumer. In turn, we will receive a `MessageConsumerEventName#DOWN` event and dispose the consumer.\n if (messageConsumer && !messageConsumer.disposed) {\n messageConsumer.disconnect();\n }\n };\n })\n .pipe(\n mapToMessageEnvelope(topicEndpointSubscription),\n synchronizeNgZone(this._zone),\n );\n }\n\n public browse$(queueOrDescriptor: string | QueueBrowserProperties): Observable<MessageEnvelope> {\n // If passed a `string` literal, connect to the given queue using default 'browsing' options.\n if (typeof queueOrDescriptor === 'string') {\n return this.createQueueBrowser$({\n queueDescriptor: new QueueDescriptor({type: QueueType.QUEUE, name: queueOrDescriptor}),\n });\n }\n\n return this.createQueueBrowser$(queueOrDescriptor);\n }\n\n private createQueueBrowser$(queueBrowserProperties: QueueBrowserProperties): Observable<MessageEnvelope> {\n return new Observable((observer: Observer<Message>): TeardownLogic => {\n let queueBrowser: QueueBrowser | undefined;\n let disposed = false;\n this.session\n .then(session => {\n queueBrowser = session.createQueueBrowser(queueBrowserProperties);\n\n // Define browser event listeners\n queueBrowser.on(QueueBrowserEventName.UP, () => {\n this._logger.debug(`QueueBrowserEvent: UP`);\n queueBrowser!.start();\n });\n queueBrowser.on(QueueBrowserEventName.CONNECT_FAILED_ERROR, (error: OperationError) => {\n this._logger.debug(`QueueBrowserEvent: CONNECT_FAILED_ERROR`, error);\n observer.error(error);\n });\n queueBrowser.on(QueueBrowserEventName.DOWN_ERROR, (error: OperationError) => {\n this._logger.debug(`QueueBrowserEvent: DOWN_ERROR`, error);\n observer.error(error);\n });\n queueBrowser.on(QueueBrowserEventName.DOWN, () => { // event emitted after successful disconnect request\n this._logger.debug(`QueueBrowserEvent: DOWN`);\n observer.complete();\n });\n queueBrowser.on(QueueBrowserEventName.DISPOSED, () => {\n this._logger.debug(`QueueBrowserEvent: DOWN`);\n disposed = true;\n observer.complete();\n });\n\n // Define browser event listener\n queueBrowser.on(QueueBrowserEventName.MESSAGE, (message: Message) => {\n this._logger.debug(`QueueBrowserEvent: MESSAGE`, message);\n NgZone.assertNotInAngularZone();\n observer.next(message);\n });\n\n // Connect the browser\n queueBrowser.connect();\n })\n .catch((error: unknown) => {\n observer.error(error);\n });\n\n return (): void => {\n // Initiate an orderly disconnection of the browser. In turn, we will receive a `QueueBrowserEventName#DOWN` event and dispose the consumer.\n if (queueBrowser && !disposed) {\n queueBrowser.stop();\n queueBrowser.disconnect();\n }\n };\n })\n .pipe(\n mapToMessageEnvelope(),\n synchronizeNgZone(this._zone),\n );\n }\n\n public publish(destination: string | Destination, data?: Data | Message, options?: PublishOptions): Promise<void> {\n const solaceDestination = typeof destination === 'string' ? SolclientFactory.createTopicDestination(destination) : destination;\n const send: Send = (session: Session, message: Message) => session.send(message);\n return this.sendMessage(solaceDestination, data, options, send);\n }\n\n public request$(destination: string | Destination, data?: Data | Message, options?: RequestOptions): Observable<MessageEnvelope> {\n const solaceDestination = typeof destination === 'string' ? SolclientFactory.createTopicDestination(destination) : destination;\n\n return new Observable<MessageEnvelope>(observer => {\n const unsubscribe$ = new Subject<void>();\n const response$ = new Subject<Message>();\n response$\n .pipe(\n assertNotInAngularZone(),\n mapToMessageEnvelope(),\n synchronizeNgZone(this._zone),\n takeUntil(unsubscribe$),\n )\n .subscribe(observer);\n\n const onResponse = (session: Session, message: Message): void => {\n response$.next(message);\n response$.complete();\n };\n const onError = (session: Session, error: RequestError): void => {\n response$.error(error);\n };\n\n const send: Send = (session: Session, request: Message) => {\n session.sendRequest(request, options?.requestTimeout, onResponse, onError);\n };\n this.sendMessage(solaceDestination, data, options, send).catch((error: unknown) => response$.error(error));\n\n return () => unsubscribe$.next();\n });\n }\n\n public reply(request: Message, data?: Data | Message, options?: PublishOptions): Promise<void> {\n // \"solclientjs\" marks the message as 'reply' and copies 'replyTo' destination and 'correlationId' from the request.\n const send: Send = (session: Session, message: Message) => session.sendReply(request, message);\n return this.sendMessage(null, data, options, send);\n }\n\n private async sendMessage(destination: Destination | null, data: ArrayBufferLike | DataView | string | SDTField | Message | undefined, options: PublishOptions | undefined, send: Send): Promise<void> {\n const message: Message = data instanceof Message ? data : SolclientFactory.createMessage();\n\n // Set the destination. May not be set if replying to a request.\n if (destination) {\n message.setDestination(destination);\n }\n\n // Set data, either as unstructured byte data, or as structured container if passed a structured data type (SDT).\n if (data !== undefined && !(data instanceof Message)) {\n if (data instanceof SDTField) {\n message.setSdtContainer(data);\n }\n else {\n message.setBinaryAttachment(data);\n }\n }\n\n // Apply publish options.\n if (options) {\n message.setDeliveryMode(options.deliveryMode ?? message.getDeliveryMode());\n message.setCorrelationId(options.correlationId ?? message.getCorrelationId());\n message.setPriority(options.priority ?? message.getPriority());\n message.setTimeToLive(options.timeToLive ?? message.getTimeToLive());\n message.setDMQEligible(options.dmqEligible ?? message.isDMQEligible());\n message.setCorrelationKey(options.correlationKey ?? message.getCorrelationKey());\n options.replyTo && message.setReplyTo(options.replyTo);\n message.setAsReplyMessage(options.markAsReply ?? message.isReplyMessage());\n }\n\n // Add headers.\n if (options?.headers?.size) {\n const userPropertyMap = (message.getUserPropertyMap() ?? new SDTMapContainer());\n options.headers.forEach((value, key) => {\n if (value === undefined || value === null) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition\n return;\n }\n if (value instanceof SDTField) {\n const sdtField = value;\n userPropertyMap.addField(key, sdtField.getType(), sdtField.getValue());\n }\n else if (typeof value === 'string') {\n userPropertyMap.addField(key, SDTFieldType.STRING, value);\n }\n else if (typeof value === 'boolean') {\n userPropertyMap.addField(key, SDTFieldType.BOOL, value);\n }\n else if (typeof value === 'number') {\n userPropertyMap.addField(key, SDTFieldType.INT32, value);\n }\n else {\n userPropertyMap.addField(key, SDTFieldType.UNKNOWN, value);\n }\n });\n message.setUserPropertyMap(userPropertyMap);\n }\n\n // Allow intercepting the message before sending it to the broker.\n options?.intercept?.(message);\n\n const session = await this.session;\n\n // Publish the message.\n if (message.getDeliveryMode() === MessageDeliveryModeType.DIRECT) {\n send(session, message);\n }\n else {\n const correlationKey = message.getCorrelationKey() ?? UUID.randomUUID();\n const whenAcknowledged = this.whenEvent(SessionEventCode.ACKNOWLEDGED_MESSAGE, {rejectOnEvent: SessionEventCode.REJECTED_MESSAGE_ERROR, correlationKey: correlationKey});\n message.setCorrelationKey(correlationKey);\n send(session, message);\n // Resolve the Promise when acknowledged by the broker, or reject it otherwise.\n await whenAcknowledged;\n }\n }\n\n public get session(): Promise<Session> {\n return this._session ?? Promise.reject(Error('Not connected to the Solace message broker. Did you forget to provide the `SolaceMessageClient` via `provideSolaceMessageClient()` function or to invoke \\'connect\\'`?'));\n }\n\n /**\n * Returns a Promise that resolves to the event when the expected event occurs, or that rejects when the specified `rejectOnEvent` event, if specified, occurs.\n * If a \"correlation key\" is specified, only events with that correlation key will be evaluated.\n *\n * Note that:\n * - the Promise resolves or rejects outside the Angular zone\n * - the Promise is bound the current session, i.e., will ony be settled as long as the current session is not disposed.\n */\n private whenEvent(resolveOnEvent: SessionEventCode, options?: {rejectOnEvent?: SessionEventCode; correlationKey?: string | object}): Promise<SessionEvent> {\n return new Promise((resolve, reject) => {\n this._event$\n .pipe(\n assertNotInAngularZone(),\n filter(event => !options?.correlationKey || event.correlationKey === options.correlationKey),\n mergeMap(event => {\n switch (event.sessionEventCode) {\n case resolveOnEvent:\n return of(event);\n case options?.rejectOnEvent: {\n return throwError(() => event);\n }\n default:\n return EMPTY;\n }\n }),\n take(1),\n )\n .subscribe({\n next: (event: SessionEvent) => resolve(event),\n error: error => reject(error as Error),\n complete: noop, // do not resolve the Promise when the session is disposed\n });\n });\n }\n\n private disposeWhenSolaceSessionDied(): void {\n this._event$\n .pipe(\n filter(event => SESSION_DIED_EVENTS.has(event.sessionEventCode)),\n assertNotInAngularZone(),\n takeUntilDestroyed(this._destroyRef),\n )\n .subscribe(() => {\n void this.dispose({force: true}); // no graceful shutdown\n });\n }\n\n private logSolaceSessionEvents(): void {\n const sessionEventCodeMapping = Object.entries(SessionEventCode).reduce((acc, [key, value]) => acc.set(value as number, key), new Map<number, string>());\n this._event$\n .pipe(\n assertNotInAngularZone(),\n takeUntilDestroyed(this._destroyRef),\n )\n .subscribe((event: SessionEvent) => {\n this._logger.debug(`SessionEvent: ${sessionEventCodeMapping.get(event.sessionEventCode)}`, event);\n });\n }\n\n private monitorConnectionState$(): Observable<boolean> {\n const connected$ = this._event$\n .pipe(\n assertNotInAngularZone(),\n map(event => event.sessionEventCode),\n filter(event => CONNECTION_ESTABLISHED_EVENTS.has(event) || CONNECTION_LOST_EVENTS.has(event)),\n map(event => CONNECTION_ESTABLISHED_EVENTS.has(event)),\n distinctUntilChanged(),\n observeIn(continueFn => this._zone.run(continueFn)),\n share({\n connector: () => new ReplaySubject<boolean>(1),\n resetOnRefCountZero: false,\n resetOnError: false,\n resetOnComplete: false,\n }),\n );\n // Connect to the source, then unsubscribe immediately (resetOnRefCountZero: false)\n connected$.subscribe().unsubscribe();\n return connected$;\n }\n\n private async loadConfig(): Promise<SolaceMessageClientConfig> {\n assertInInjectionContext(this.loadConfig); // config function must be called inside an injection context\n\n const configLike = inject(SOLACE_MESSAGE_CLIENT_CONFIG);\n const config = typeof configLike === 'function' ? await firstValueFrom(Observables.coerce(configLike())) : configLike;\n\n // Apply Solace session defaults.\n return {\n reapplySubscriptions: true, // remember subscriptions after a network interruption (default value if not set)\n reconnectRetries: -1, // Try to restore the connection automatically after a network interruption (default value if not set)\n ...config,\n };\n }\n}\n\n/**\n * Maps each {@link Message} to a {@link MessageEnvelope}, and resolves substituted named wildcard segments.\n */\nfunction mapToMessageEnvelope(subscriptionTopic?: string): OperatorFunction<Message, MessageEnvelope> {\n return map((message: Message): MessageEnvelope => {\n return {\n message,\n params: collectNamedTopicSegmentValues(message, subscriptionTopic),\n headers: collectHeaders(message),\n };\n });\n}\n\n/**\n * Collects message headers from given message.\n */\nfunction collectHeaders(message: Message): Map<string, unknown> {\n const userPropertyMap = message.getUserPropertyMap();\n return userPropertyMap?.getKeys().reduce((acc, key) => {\n return acc.set(key, userPropertyMap.getField(key).getValue());\n }, new Map<string, unknown>()) ?? new Map<string, unknown>();\n}\n\n/**\n * Parses the effective message destination for named path segments, if any.\n */\nfunction collectNamedTopicSegmentValues(message: Message, subscriptionTopic: string | undefined): Map<string, string> {\n if (!subscriptionTopic?.length) {\n return new Map<string, string>();\n }\n\n const subscriptionSegments = subscriptionTopic.split('/');\n const effectiveDestinationSegments = message.getDestination()!.getName().split('/');\n return subscriptionSegments.reduce((acc, subscriptionSegment, i) => {\n if (isNamedWildcardSegment(subscriptionSegment)) {\n return acc.set(subscriptionSegment.substring(1), effectiveDestinationSegments[i]!);\n }\n return acc;\n }, new Map<string, string>());\n}\n\n/**\n * Tests whether given segment is a named path segment, i.e., a segment that acts as placeholer for any value, equivalent to the Solace single-level wildcard character (`*`).\n */\nfunction isNamedWildcardSegment(segment: string): boolean {\n return segment.startsWith(':') && segment.length > 1;\n}\n\n/**\n * Creates a Solace subscription topic with named topic segments replaced by single-level wildcard characters (`*`), if any.\n */\nfunction createSubscriptionTopicDestination(topic: string): Destination {\n const subscriptionTopic = topic.split('/')\n .map(segment => isNamedWildcardSegment(segment) ? '*' : segment)\n .join('/');\n return SolclientFactory.createTopicDestination(subscriptionTopic);\n}\n\n/**\n * Set of events indicating final disconnection from the broker with no recovery possible.\n */\nconst SESSION_DIED_EVENTS = new Set<number>()\n .add(SessionEventCode.DOWN_ERROR) // is emitted when reaching the limit of connection retries after a connection interruption\n .add(SessionEventCode.DISCONNECTED); // is emitted when disconnected from the session\n/**\n * Set of events indicating a connection to be established.\n */\nconst CONNECTION_ESTABLISHED_EVENTS = new Set<number>()\n .add(SessionEventCode.UP_NOTICE)\n .add(SessionEventCode.RECONNECTED_NOTICE);\n\n/**\n * Set of events indicating a connection to be lost.\n */\nconst CONNECTION_LOST_EVENTS = new Set<number>()\n .add(SessionEventCode.DOWN_ERROR)\n .add(SessionEventCode.CONNECT_FAILED_ERROR)\n .add(SessionEventCode.DISCONNECTED)\n .add(SessionEventCode.RECONNECTING_NOTICE);\n\n/**\n * Throws if emitting inside the Angular zone.\n */\nfunction assertNotInAngularZone<T>(): MonoTypeOperatorFunction<T> {\n return tap(() => NgZone.assertNotInAngularZone());\n}\n\n/**\n * Implements a strategy for sending a message.\n */\ntype Send = (session: Session, message: Message) => void;\n\nfunction obfuscateSecrets(sessionProperties: SolaceMessageClientConfig): SolaceMessageClientConfig {\n const obfuscated = {...sessionProperties};\n if (obfuscated.password) {\n obfuscated.password = '***';\n }\n if (obfuscated.accessToken) {\n obfuscated.accessToken = '***';\n }\n return obfuscated;\n}\n\n/**\n * Maps RxJS {@link EmptyError} to the given error. Other errors are re-thrown unchanged.\n */\nfunction mapEmptyError(errorFactory: () => Error): (error: unknown) => Promise<never> {\n return (error: unknown): Promise<never> => {\n if (error instanceof EmptyError) {\n return Promise.reject(errorFactory());\n }\n else {\n return Promise.reject(error as Error);\n }\n };\n}\n\n/**\n * Emits in the zone in which subscribed to the Observable.\n */\nfunction synchronizeNgZone<T>(zone: NgZone): MonoTypeOperatorFunction<T> {\n return (source$: Observable<T>) => {\n return new Observable<T>(observer => {\n const insideAngular = NgZone.isInAngularZone();\n const subscription = source$\n .pipe(observeIn(fn => insideAngular ? runInsideAngular(fn) : runOutsideAngular(fn)))\n .subscribe(observer);\n return () => subscription.unsubscribe();\n });\n };\n\n function runInsideAngular(fn: () => void): void {\n NgZone.isInAngularZone() ? fn() : zone.run(fn);\n }\n\n function runOutsideAngular(fn: () => void): void {\n !NgZone.isInAngularZone() ? fn() : zone.runOutsideAngular(fn);\n }\n}\n\n/**\n * Provides the OAuth 2.0 access token configured in {@link SolaceMessageClientConfig#accessToken} as Observable.\n * The Observable errors if not using OAUTH2 authentication scheme, or if no token/provider is configured.\n */\nfunction provideOAuthAccessToken$(config: SolaceMessageClientConfig, options: {injector: Injector}): Observable<string> {\n if (config.authenticationScheme !== AuthenticationScheme.OAUTH2) {\n return throwError(() => `Expected authentication scheme to be ${AuthenticationScheme.OAUTH2}, but was ${config.authenticationScheme}.`);\n }\n\n return runInInjectionContext(options.injector, () => {\n const zone = inject(NgZone);\n const configuredAccessToken = config.accessToken;\n const accessToken$ = zone.run(() => {\n switch (typeof configuredAccessToken) {\n case 'string': {\n return of(configuredAccessToken);\n }\n case 'function': {\n return Observables.coerce(configuredAccessToken());\n }\n default: {\n return throwError(() => Error('[NullAccessTokenConfigError] No access token or provider function configured in \\'SolaceMessageClientConfig.accessToken\\'. An access token is required for OAUTH2 authentication. It is recommended to provide the token via `OAuthAccessTokenFn`.'));\n }\n }\n });\n\n return accessToken$.pipe(mergeMap(accessToken => accessToken ? of(accessToken) : throwError(() => Error('[NullAccessTokenError] Invalid \"OAuth 2.0 Access Token\". Token must not be `null` or `undefined`.'))));\n });\n}\n","import {Injectable} from '@angular/core';\nimport {BehaviorSubject, NEVER, noop, Observable, OperatorFunction} from 'rxjs';\nimport {Destination, Message, MessageCons