UNPKG

@stomp/rx-stomp

Version:

RxJS STOMP client for Javascript and Typescript

527 lines (519 loc) 23.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('rxjs'), require('@stomp/stompjs'), require('uuid')) : typeof define === 'function' && define.amd ? define(['exports', 'rxjs', '@stomp/stompjs', 'uuid'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.RxStomp = {}, global.rxjs, global.StompJs, global.uuid)); })(this, (function (exports, rxjs, stompjs, uuid) { 'use strict'; /** * Represents a configuration object for RxSTOMP. * Instance of this can be passed to [RxStomp#configure]{@link RxStomp#configure} * * All the attributes of these calls are optional. * * Part of `@stomp/rx-stomp` */ class RxStompConfig { } /** * Possible states for the RxStomp * * Part of `@stomp/rx-stomp` */ exports.RxStompState = void 0; (function (RxStompState) { RxStompState[RxStompState["CONNECTING"] = 0] = "CONNECTING"; RxStompState[RxStompState["OPEN"] = 1] = "OPEN"; RxStompState[RxStompState["CLOSING"] = 2] = "CLOSING"; RxStompState[RxStompState["CLOSED"] = 3] = "CLOSED"; })(exports.RxStompState || (exports.RxStompState = {})); /** * This is the main Stomp Client. * Typically, you will create an instance of this to connect to the STOMP broker. * * This wraps an instance of [@stomp/stompjs]{@link https://github.com/stomp-js/stompjs} * {@link Client}. * * The key difference is that it exposes operations as RxJS Observables. * For example, when a STOMP endpoint is subscribed it returns an Observable * that will stream all received messages. * * With exception to beforeConnect, functionality related to all callbacks in * [@stomp/stompjs Client]{@link Client} * is exposed as Observables/Subjects/BehaviorSubjects. * * RxStomp also tries to transparently handle connection failures. * * Part of `@stomp/rx-stomp` */ class RxStomp { /** * Instance of actual * [@stomp/stompjs]{@link https://github.com/stomp-js/stompjs} * {@link Client}. * * **Be careful in calling methods on it directly - you may get unintended consequences.** */ get stompClient() { return this._stompClient; } /** * Constructor * * @param stompClient optionally inject the * [@stomp/stompjs]{@link https://github.com/stomp-js/stompjs} * {@link Client} to wrap. If this is not provided, a client will * be constructed internally. */ constructor(stompClient) { /** * Internal array to hold locally queued messages when STOMP broker is not connected. */ this._queuedMessages = []; const client = stompClient ? stompClient : new stompjs.Client(); this._stompClient = client; const noOp = () => { }; // Before connect is no op by default this._beforeConnect = noOp; // Correlate errors is falsey op by default this._correlateErrors = () => undefined; // debug is no-op by default this._debug = noOp; // Initial state is CLOSED this._connectionStatePre$ = new rxjs.BehaviorSubject(exports.RxStompState.CLOSED); this._connectedPre$ = this._connectionStatePre$.pipe(rxjs.filter((currentState) => { return currentState === exports.RxStompState.OPEN; })); // Initial state is CLOSED this.connectionState$ = new rxjs.BehaviorSubject(exports.RxStompState.CLOSED); this.connected$ = this.connectionState$.pipe(rxjs.filter((currentState) => { return currentState === exports.RxStompState.OPEN; })); // Setup sending queuedMessages this.connected$.subscribe(() => { this._sendQueuedMessages(); }); this._serverHeadersBehaviourSubject$ = new rxjs.BehaviorSubject(null); this.serverHeaders$ = this._serverHeadersBehaviourSubject$.pipe(rxjs.filter((headers) => { return headers !== null; })); this.stompErrors$ = new rxjs.Subject(); this.unhandledMessage$ = new rxjs.Subject(); this.unhandledReceipts$ = new rxjs.Subject(); this.unhandledFrame$ = new rxjs.Subject(); this.webSocketErrors$ = new rxjs.Subject(); } /** * Set configuration. This method may be called multiple times. * Each call will add to the existing configuration. * * Example: * * ```javascript * const rxStomp = new RxStomp(); * rxStomp.configure({ * brokerURL: 'ws://127.0.0.1:15674/ws', * connectHeaders: { * login: 'guest', * passcode: 'guest' * }, * heartbeatIncoming: 0, * heartbeatOutgoing: 20000, * reconnectDelay: 200, * debug: (msg: string): void => { * console.log(new Date(), msg); * } * }); * rxStomp.activate(); * ``` * * Maps to: [Client#configure]{@link Client#configure} */ configure(rxStompConfig) { const stompConfig = Object.assign({}, rxStompConfig); if (stompConfig.beforeConnect) { this._beforeConnect = stompConfig.beforeConnect; delete stompConfig.beforeConnect; } if (stompConfig.correlateErrors) { this._correlateErrors = stompConfig.correlateErrors; delete stompConfig.correlateErrors; } // RxStompConfig has subset of StompConfig fields this._stompClient.configure(stompConfig); if (stompConfig.debug) { this._debug = stompConfig.debug; } } /** * Initiate the connection with the broker. * If the connection breaks, as per [RxStompConfig#reconnectDelay]{@link RxStompConfig#reconnectDelay}, * it will keep trying to reconnect. * * Call [RxStomp#deactivate]{@link RxStomp#deactivate} to disconnect and stop reconnection attempts. * * Maps to: [Client#activate]{@link Client#activate} */ activate() { this._stompClient.configure({ beforeConnect: async () => { this._changeState(exports.RxStompState.CONNECTING); // Call handler await this._beforeConnect(this); }, onConnect: (frame) => { this._serverHeadersBehaviourSubject$.next(frame.headers); // Indicate our connected state to observers this._changeState(exports.RxStompState.OPEN); }, onStompError: (frame) => { // Trigger the frame subject this.stompErrors$.next(frame); }, onWebSocketClose: () => { this._changeState(exports.RxStompState.CLOSED); }, onUnhandledMessage: (message) => { this.unhandledMessage$.next(message); }, onUnhandledReceipt: (frame) => { this.unhandledReceipts$.next(frame); }, onUnhandledFrame: (frame) => { this.unhandledFrame$.next(frame); }, onWebSocketError: (evt) => { this.webSocketErrors$.next(evt); }, }); // Attempt connection this._stompClient.activate(); } /** * Disconnect if connected and stop auto reconnect loop. * Appropriate callbacks will be invoked if the underlying STOMP connection was connected. * * To reactivate, you can call [RxStomp#activate]{@link RxStomp#activate}. * * This call is async. It will resolve immediately if there is no underlying active websocket, * otherwise, it will resolve after the underlying websocket is properly disposed of. * * Experimental: Since version 2.0.0, pass `force: true` to immediately discard the underlying connection. * See [Client#deactivate]{@link Client#deactivate} for details. * * Maps to: [Client#deactivate]{@link Client#deactivate} */ async deactivate(options = {}) { this._changeState(exports.RxStompState.CLOSING); // The promise will be resolved immediately if there is no active connection // otherwise, after it has successfully disconnected. await this._stompClient.deactivate(options); this._changeState(exports.RxStompState.CLOSED); } /** * It will return `true` if STOMP broker is connected and `false` otherwise. */ connected() { return this.connectionState$.getValue() === exports.RxStompState.OPEN; } /** * If the client is active (connected or going to reconnect). * * Maps to: [Client#active]{@link Client#active} */ get active() { return this.stompClient.active; } /** * Send a message to a named destination. Refer to your STOMP broker documentation for types * and naming of destinations. * * STOMP protocol specifies and suggests some headers and also allows broker-specific headers. * * `body` must be String. * You will need to covert the payload to string in case it is not string (e.g. JSON). * * To send a binary message body, use binaryBody parameter. It should be a * [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array). * Sometimes brokers may not support binary frames out of the box. * Please check your broker documentation. * * The ` content-length` header is automatically added to the STOMP Frame sent to the broker. * Set `skipContentLengthHeader` to indicate that `content-length` header should not be added. * For binary messages, `content-length` header is always added. * * Caution: The broker will, most likely, report an error and disconnect if the message body has NULL octet(s) * and `content-length` header is missing. * * The message will get locally queued if the STOMP broker is not connected. It will attempt to * publish queued messages as soon as the broker gets connected. * If you do not want that behavior, * please set [retryIfDisconnected]{@link IRxStompPublishParams#retryIfDisconnected} to `false` * in the parameters. * When `false`, this function will raise an error if a message could not be sent immediately. * * Maps to: [Client#publish]{@link Client#publish} * * See: {@link IRxStompPublishParams} and {@link IPublishParams} * * ```javascript * rxStomp.publish({destination: "/queue/test", headers: {priority: 9}, body: "Hello, STOMP"}); * * // Only destination is mandatory parameter * rxStomp.publish({destination: "/queue/test", body: "Hello, STOMP"}); * * // Skip content-length header in the frame to the broker * rxStomp.publish({"/queue/test", body: "Hello, STOMP", skipContentLengthHeader: true}); * * var binaryData = generateBinaryData(); // This need to be of type Uint8Array * // setting content-type header is not mandatory, however a good practice * rxStomp.publish({destination: '/topic/special', binaryBody: binaryData, * headers: {'content-type': 'application/octet-stream'}}); * ``` */ publish(parameters) { // retry behaviour is defaulted to true const shouldRetry = parameters.retryIfDisconnected == null ? true : parameters.retryIfDisconnected; if (this.connected()) { this._stompClient.publish(parameters); } else if (shouldRetry) { this._debug(`Not connected, queueing`); this._queuedMessages.push(parameters); } else { throw new Error('Cannot publish while broker is not connected'); } } /** It will send queued messages. */ _sendQueuedMessages() { const queuedMessages = this._queuedMessages; this._queuedMessages = []; if (queuedMessages.length === 0) { return; } this._debug(`Will try sending ${queuedMessages.length} queued message(s)`); for (const queuedMessage of queuedMessages) { this._debug(`Attempting to send ${queuedMessage}`); this.publish(queuedMessage); } } watch(opts, headers = {}) { const defaults = { subHeaders: {}, unsubHeaders: {}, subscribeOnlyOnce: false, }; let params; if (typeof opts === 'string') { params = Object.assign({}, defaults, { destination: opts, subHeaders: headers, }); } else { params = Object.assign({}, defaults, opts); } /* Well, the logic is complicated but works beautifully. RxJS is indeed wonderful. * * We need to activate the underlying subscription immediately if Stomp is connected. If not, it should * subscribe when it gets next connected. Further, it should re-establish the subscription whenever Stomp * successfully reconnects. * * Actual implementation is simple, we filter the BehaviourSubject 'state' so that we can trigger whenever Stomp is * connected. Since 'state' is a BehaviourSubject, if Stomp is already connected, it will immediately trigger. * * The observable that we return to the caller remains the same across all reconnects, so no special handling needed at * the message subscriber. */ this._debug(`Request to subscribe ${params.destination}`); const coldObservable = rxjs.Observable.create((messages) => { /* * These variables will be used as part of the closure and work their magic during unsubscribe */ let stompSubscription; // Stomp let stompConnectedSubscription; // RxJS let connectedPre$ = this._connectedPre$; if (params.subscribeOnlyOnce) { connectedPre$ = connectedPre$.pipe(rxjs.take(1)); } const stompErrorsSubscription = this.stompErrors$.subscribe((error) => { const correlatedDestination = this._correlateErrors(error); if (correlatedDestination === params.destination) { messages.error(error); } }); stompConnectedSubscription = connectedPre$.subscribe(() => { this._debug(`Will subscribe to ${params.destination}`); let subHeaders = params.subHeaders; if (typeof subHeaders === 'function') { subHeaders = subHeaders(); } stompSubscription = this._stompClient.subscribe(params.destination, (message) => { messages.next(message); }, subHeaders); }); return () => { /* cleanup function, it will be called when no subscribers are left */ this._debug(`Stop watching connection state (for ${params.destination})`); stompConnectedSubscription.unsubscribe(); stompErrorsSubscription.unsubscribe(); if (this.connected()) { this._debug(`Will unsubscribe from ${params.destination} at Stomp`); let unsubHeaders = params.unsubHeaders; if (typeof unsubHeaders === 'function') { unsubHeaders = unsubHeaders(); } stompSubscription.unsubscribe(unsubHeaders); } else { this._debug(`Stomp not connected, no need to unsubscribe from ${params.destination} at Stomp`); } }; }); /** * Important - convert it to hot Observable - otherwise, if the user code subscribes * to this observable twice, it will subscribe twice to Stomp broker. (This was happening in the current example). * A long but good explanatory article at https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339 */ return coldObservable.pipe(rxjs.share()); } /** * **Deprecated** Please use {@link asyncReceipt}. */ watchForReceipt(receiptId, callback) { this._stompClient.watchForReceipt(receiptId, callback); } /** * STOMP brokers may carry out operation asynchronously and allow requesting for acknowledgement. * To request an acknowledgement, a `receipt` header needs to be sent with the actual request. * The value (say receipt-id) for this header needs to be unique for each use. Typically, a sequence, a UUID, a * random number or a combination may be used. * * A complaint broker will send a RECEIPT frame when an operation has actually been completed. * The operation needs to be matched based on the value of the receipt-id. * * This method allows watching for a receipt and invoking the callback * when the corresponding receipt has been received. * * The promise will yield the actual {@link IFrame}. * * Example: * ```javascript * // Publishing with acknowledgement * let receiptId = randomText(); * * rxStomp.publish({destination: '/topic/special', headers: {receipt: receiptId}, body: msg}); * await rxStomp.asyncReceipt(receiptId);; // it yields the actual Frame * ``` * * Maps to: [Client#watchForReceipt]{@link Client#watchForReceipt} */ asyncReceipt(receiptId) { return rxjs.firstValueFrom(this.unhandledReceipts$.pipe(rxjs.filter(frame => frame.headers['receipt-id'] === receiptId))); } _changeState(state) { this._connectionStatePre$.next(state); this.connectionState$.next(state); } } /** * RPC Config. For examples see the [guide](/guide/rx-stomp/ng2-stompjs/remote-procedure-call.html). */ class RxStompRPCConfig { } /** * An implementation of Remote Procedure Call (RPC) using messaging. * * Please see the [guide](/guide/rx-stomp/ng2-stompjs/remote-procedure-call.html) for details. * * Part of `@stomp/rx-stomp` */ class RxStompRPC { /** * Create an instance, see the [guide](/guide/rx-stomp/ng2-stompjs/remote-procedure-call.html) for details. */ constructor(rxStomp, stompRPCConfig) { this.rxStomp = rxStomp; this.stompRPCConfig = stompRPCConfig; this._replyQueueName = '/temp-queue/rpc-replies'; this._setupReplyQueue = () => { return this.rxStomp.unhandledMessage$; }; this._customReplyQueue = false; if (stompRPCConfig) { if (stompRPCConfig.replyQueueName) { this._replyQueueName = stompRPCConfig.replyQueueName; } if (stompRPCConfig.setupReplyQueue) { this._customReplyQueue = true; this._setupReplyQueue = stompRPCConfig.setupReplyQueue; } } } /** * Make an RPC request. * See the [guide](/guide/rx-stomp/ng2-stompjs/remote-procedure-call.html) for example. * * It is a simple wrapper around [RxStompRPC#stream]{@link RxStompRPC#stream}. */ rpc(params) { // We know there will be only one message in reply return this.stream(params).pipe(rxjs.first()); } /** * Make an RPC stream request. See the [guide](/guide/rx-stomp/ng2-stompjs/remote-procedure-call.html). * * Note: This call internally takes care of generating a correlation id, * however, if `correlation-id` is passed via `headers`, that will be used instead. */ stream(params) { // defensively copy const headers = { ...(params.headers || {}) }; if (!this._repliesObservable) { const repliesObservable = this._setupReplyQueue(this._replyQueueName, this.rxStomp); // In case of custom queue, ensure it remains subscribed if (this._customReplyQueue) { this._dummySubscription = repliesObservable.subscribe(() => { }); } this._repliesObservable = repliesObservable; } return rxjs.Observable.create((rpcObserver) => { let defaultMessagesSubscription; const correlationId = headers['correlation-id'] || uuid.v4(); defaultMessagesSubscription = this._repliesObservable .pipe(rxjs.filter((message) => { return message.headers['correlation-id'] === correlationId; })) .subscribe((message) => { rpcObserver.next(message); }); // send an RPC request headers['reply-to'] = this._replyQueueName; headers['correlation-id'] = correlationId; this.rxStomp.publish({ ...params, headers }); return () => { // Cleanup defaultMessagesSubscription.unsubscribe(); }; }); } } Object.defineProperty(exports, "StompHeaders", { enumerable: true, get: function () { return stompjs.StompHeaders; } }); Object.defineProperty(exports, "StompSocketState", { enumerable: true, get: function () { return stompjs.StompSocketState; } }); Object.defineProperty(exports, "Versions", { enumerable: true, get: function () { return stompjs.Versions; } }); exports.RxStomp = RxStomp; exports.RxStompConfig = RxStompConfig; exports.RxStompRPC = RxStompRPC; exports.RxStompRPCConfig = RxStompRPCConfig; }));