UNPKG

wampy

Version:

Amazingly fast, feature-rich, lightweight WAMP Javascript client (for browser and node.js)

1 lines 125 kB
{"version":3,"sources":["../../src/wampy.ts","../../src/constants.ts","../../src/errors.ts","../../src/utils.ts","../../src/serializers/json-serializer.ts"],"sourcesContent":["/**\n * Project: wampy.js\n *\n * https://github.com/KSDaemon/wampy.js\n *\n * A lightweight client-side implementation of\n * WAMP (The WebSocket Application Messaging Protocol v2)\n * https://wamp-proto.org\n *\n * Provides asynchronous RPC/PubSub over WebSocket.\n *\n * Copyright 2014 KSDaemon. Licensed under the MIT License.\n * See @license text at http://www.opensource.org/licenses/mit-license.php\n *\n */\n\nimport { E2EE_SERIALIZERS, SUCCESS, WAMP_ERROR_MSG, WAMP_MSG_SPEC, WAMP_CUSTOM_ATTR_REGEX } from './constants.js';\nimport * as Errors from './errors.js';\nimport { WebsocketError } from './errors.js';\nimport { getNewPromise, getWebSocket } from './utils.js';\nimport { JsonSerializer } from './serializers/json-serializer.js';\nimport type { Serializer } from './serializers/serializer.js';\nimport type {\n WampyOptions,\n WampyCache,\n WampyOpStatus,\n WampFeatures,\n SubscriptionCallbacksHash,\n RegistrationCallbacksHash,\n WampRequest,\n WampCall,\n TopicType,\n WampRole,\n Payload,\n PayloadWithArgsKwargs,\n PackPPTPayloadResult,\n UnpackPPTPayloadResult,\n EventCallback,\n RPCCallback,\n CallResult,\n InvocationResult,\n InvocationErrorData,\n SubscribeAdvancedOptions,\n PublishAdvancedOptions,\n CallAdvancedOptions,\n CancelAdvancedOptions,\n RegisterAdvancedOptions,\n ProgressiveCallSendDataOptions,\n ProgressiveCallReturn,\n SubscribeSuccessResult,\n UnsubscribeSuccessResult,\n PublishSuccessResult,\n RegisterSuccessResult,\n UnregisterSuccessResult,\n SubscribeRequestCallbacks,\n RegisterRequestCallbacks,\n SubscribeRequest,\n UnsubscribeRequest,\n PublishRequest,\n RegisterRequest,\n UnregisterRequest,\n ServerWampFeatures,\n} from './types.js';\n\nconst jsonSerializer: Serializer = new JsonSerializer();\n\n/**\n * WAMP Client Class\n */\nclass Wampy {\n\n /** Wampy version */\n version: string = 'v8.0.0';\n\n /** WS Url */\n private _url: string | null;\n\n /** WS protocols */\n private _protocols: string[];\n\n /** WAMP features, supported by Wampy */\n private readonly _wamp_features: WampFeatures;\n\n /** Internal cache for object lifetime */\n private _cache: WampyCache;\n\n /** WebSocket object */\n private _ws: WebSocket | null;\n\n /** Internal queue for websocket requests, for case of disconnect */\n private _wsQueue: (string | ArrayBuffer | Uint8Array | undefined)[];\n\n /** Internal queue for wamp requests */\n private _requests: Record<number, WampRequest>;\n\n /** Stored RPC */\n private _calls: Record<number, WampCall>;\n\n /** Stored Pub/Subs to access by ID */\n private readonly _subscriptionsById: Map<number, SubscriptionCallbacksHash>;\n\n /** Stored Pub/Subs to access by Key */\n private _subscriptionsByKey: Map<string, SubscriptionCallbacksHash>;\n\n /** Stored RPC Registrations */\n private _rpcRegs: Record<string | number, RegistrationCallbacksHash>;\n\n /** Stored RPC names */\n private _rpcNames: Set<string>;\n\n /** Options hash-table */\n private _options: Required<WampyOptions>;\n\n constructor ();\n constructor (url: string);\n constructor (options: WampyOptions);\n constructor (url: string, options: WampyOptions);\n constructor (url?: string | WampyOptions, options?: WampyOptions) {\n\n this._url = (typeof url === 'string') ? url : null;\n\n this._protocols = ['wamp.2.json'];\n\n this._wamp_features = {\n agent: 'Wampy.js ' + this.version,\n roles: {\n publisher : {\n features: {\n subscriber_blackwhite_listing: true,\n publisher_exclusion : true,\n publisher_identification : true,\n payload_passthru_mode : true\n }\n },\n subscriber: {\n features: {\n pattern_based_subscription: true,\n publication_trustlevels : true,\n publisher_identification : true,\n payload_passthru_mode : true\n }\n },\n caller : {\n features: {\n caller_identification : true,\n progressive_call_results: true,\n call_canceling : true,\n call_timeout : true,\n payload_passthru_mode : true\n }\n },\n callee : {\n features: {\n caller_identification : true,\n call_trustlevels : true,\n pattern_based_registration: true,\n shared_registration : true,\n payload_passthru_mode : true\n\n }\n }\n }\n };\n\n this._cache = {\n sessionId: null,\n reqId: 0,\n server_wamp_features: { roles: {} },\n isSayingGoodbye: false,\n opStatus: {\n code: 0,\n error: null,\n reqId: 0\n },\n timer: null,\n reconnectingAttempts: 0,\n connectPromise: null,\n closePromise: null\n };\n\n this._ws = null;\n this._wsQueue = [];\n this._requests = {};\n this._calls = {};\n this._subscriptionsById = new Map();\n this._subscriptionsByKey = new Map();\n this._rpcRegs = {};\n this._rpcNames = new Set();\n\n this._options = {\n debug: false,\n logger: null,\n autoReconnect: true,\n reconnectInterval: 2 * 1000,\n maxRetries: 25,\n realm: null,\n helloCustomDetails: null,\n uriValidation: 'strict',\n authid: null,\n authmethods: [],\n authextra: {},\n authPlugins: {},\n authMode: 'manual',\n onChallenge: null,\n onClose: null,\n onError: null,\n onReconnect: null,\n onReconnectSuccess: null,\n ws: null,\n additionalHeaders: null,\n wsRequestOptions: null,\n serializer: jsonSerializer,\n payloadSerializers: {\n json: jsonSerializer\n }\n };\n\n if (this._isPlainObject(options)) {\n this._options = { ...this._options, ...options as WampyOptions };\n } else if (this._isPlainObject(url)) {\n this._options = { ...this._options, ...url as WampyOptions };\n }\n }\n\n /* Internal utils methods */\n\n /** Internal logger */\n private _log (...args: unknown[]): void {\n if (!this._options.debug) { return; }\n\n if (this._options.logger) {\n return this._options.logger(args);\n }\n\n return console.log('[wampy]', args);\n }\n\n /** Get the new unique request id */\n private _getReqId (): number {\n return ++this._cache.reqId;\n }\n\n /** Check if input is an object literal */\n private _isPlainObject (input: unknown): input is Record<string, unknown> {\n const constructor = (input as Record<string, unknown>)?.constructor;\n const prototype = (constructor as { prototype?: unknown })?.prototype;\n\n return Object.prototype.toString.call(input) === '[object Object]' // checks for primitives, null, Arrays, DOM, etc.\n && typeof constructor === 'function' // checks for modified constructors\n && Object.prototype.toString.call(prototype) === '[object Object]' // checks for modified prototypes\n && Object.hasOwnProperty.call(prototype, 'isPrototypeOf'); // checks for missing object-specific property\n }\n\n /** Set websocket protocol based on options */\n private _setWsProtocols (): void {\n this._protocols = ['wamp.2.' + this._options.serializer.protocol];\n // FIXME: Temporary commented out due to bug in Nexus\n // if (!(this._options.serializer instanceof JsonSerializer)) {\n // this._protocols.unshift('wamp.2.' + this._options.serializer.protocol);\n // }\n }\n\n /** Fill instance operation status */\n private _fillOpStatusByError (err: Error & { code: number }): void {\n this._cache.opStatus = {\n code: err.code,\n error: err,\n reqId: 0\n };\n }\n\n /** Prerequisite checks for any wampy api call */\n private _preReqChecks (topicType: TopicType | null, role: WampRole): boolean {\n if (this._cache.sessionId && !this._cache.server_wamp_features.roles[role]) {\n const errorsByRole: Record<WampRole, Error & { code: number }> = {\n dealer: new Errors.NoDealerError(),\n broker: new Errors.NoBrokerError(),\n };\n\n this._fillOpStatusByError(errorsByRole[role]);\n return false;\n }\n\n if (topicType && !this._validateURI(topicType.topic, topicType.patternBased, topicType.allowWAMP)) {\n this._fillOpStatusByError(new Errors.UriError());\n return false;\n }\n\n return true;\n }\n\n /** Check for specified feature in a role of connected WAMP Router */\n private _checkRouterFeature (role: string, feature: string): boolean {\n if (!this._cache.server_wamp_features.roles[role].features[feature]) {\n this._fillOpStatusByError(new Errors.FeatureNotSupportedError(role, feature));\n return false;\n }\n\n return true;\n }\n\n /** Check for PPT mode options correctness */\n private _checkPPTOptions (role: string, options: Record<string, unknown>): boolean {\n if (!this._checkRouterFeature(role, 'payload_passthru_mode')) {\n this._fillOpStatusByError(new Errors.PPTNotSupportedError());\n return false;\n }\n\n if ((options.ppt_scheme as string).search(/^(wamp$|mqtt$|x_)/) < 0) {\n this._fillOpStatusByError(new Errors.PPTInvalidSchemeError());\n return false;\n }\n\n if (options.ppt_scheme === 'wamp' && !E2EE_SERIALIZERS.includes(options.ppt_serializer as string)) {\n this._fillOpStatusByError(new Errors.PPTSerializerInvalidError());\n return false;\n }\n\n return true;\n }\n\n /** Validate uri */\n private _validateURI (uri: string, isPatternBased: boolean, isWampAllowed: boolean): boolean {\n const isStrictValidation = this._options.uriValidation === 'strict';\n const isLooseValidation = this._options.uriValidation === 'loose';\n const isValidationTypeUnknown = !isStrictValidation && !isLooseValidation;\n\n if (isValidationTypeUnknown || (uri.startsWith('wamp.') && !isWampAllowed)) {\n return false;\n }\n\n let reBase: RegExp | undefined, rePattern: RegExp | undefined;\n if (isStrictValidation) {\n reBase = /^(\\w+\\.)*(\\w+)$/;\n rePattern = /^(\\w+\\.{1,2})*(\\w+)$/;\n } else if (isLooseValidation) {\n reBase = /^([^\\s#.]+\\.)*([^\\s#.]+)$/;\n rePattern = /^([^\\s#.]+\\.{1,2})*([^\\s#.]+)$/;\n }\n\n return (isPatternBased ? rePattern! : reBase!).test(uri);\n }\n\n /** Prepares PPT/E2EE payload for adding to WAMP message */\n private _packPPTPayload (payload: Payload, options: Record<string, unknown>): PackPPTPayloadResult {\n const payloadObj = payload as PayloadWithArgsKwargs;\n const isArgsListInvalid = payloadObj?.argsList && !Array.isArray(payloadObj.argsList);\n const isArgsDictInvalid = payloadObj?.argsDict && !this._isPlainObject(payloadObj.argsDict);\n\n if (isArgsListInvalid || isArgsDictInvalid) {\n const invalidParameter = isArgsListInvalid ? payloadObj.argsList : payloadObj.argsDict;\n this._fillOpStatusByError(new Errors.InvalidParamError(String(invalidParameter)));\n return { err: true, payloadItems: [] };\n }\n\n const isPayloadAnObject = this._isPlainObject(payload);\n const { argsList, argsDict } = payloadObj ?? {};\n let args: unknown[] | undefined, kwargs: Record<string, unknown> | undefined;\n\n if (isPayloadAnObject && !argsList && !argsDict) {\n kwargs = payload as Record<string, unknown>;\n } else if (isPayloadAnObject) {\n args = argsList;\n kwargs = argsDict;\n } else if (Array.isArray(payload)) {\n args = payload;\n } else { // assume it's a single value\n args = [payload];\n }\n\n const payloadItems: unknown[] = [];\n\n if (!options.ppt_scheme) {\n if (args) {\n payloadItems.push(args);\n }\n if (kwargs) {\n if (!args) {\n payloadItems.push([]);\n }\n payloadItems.push(kwargs);\n }\n return { err: false, payloadItems };\n }\n\n const pptPayload = { args, kwargs };\n let binPayload: unknown = pptPayload;\n\n // Check and handle Payload PassThru Mode\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-passthru-mode\n if (options.ppt_serializer && options.ppt_serializer !== 'native') {\n const pptSerializer = this._options.payloadSerializers[options.ppt_serializer as string];\n\n if (!pptSerializer) {\n this._fillOpStatusByError(new Errors.PPTSerializerInvalidError());\n return { err: true, payloadItems };\n }\n\n try {\n binPayload = pptSerializer.encode(pptPayload);\n } catch {\n this._fillOpStatusByError(new Errors.PPTSerializationError());\n return { err: true, payloadItems };\n }\n }\n\n // TODO: implement End-to-End Encryption\n // wamp scheme means Payload End-to-End Encryption\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-end-to-end-encrypti\n // if (options.ppt_scheme === 'wamp') {\n //\n // }\n\n payloadItems.push([binPayload]);\n\n return { err: false, payloadItems };\n }\n\n /** Unpack PPT/E2EE payload to common */\n private _unpackPPTPayload (role: string, pptPayload: unknown, options: Record<string, unknown>): UnpackPPTPayloadResult {\n let decodedPayload: { args?: unknown[]; kwargs?: Record<string, unknown> };\n\n if (!this._checkPPTOptions(role, options)) {\n return { err: this._cache.opStatus.error || false };\n }\n\n // TODO: implement End-to-End Encryption\n // wamp scheme means Payload End-to-End Encryption\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-end-to-end-encrypti\n // if (options.ppt_scheme === 'wamp') {\n //\n // }\n\n if (options.ppt_serializer && options.ppt_serializer !== 'native') {\n const pptSerializer = this._options.payloadSerializers[options.ppt_serializer as string];\n\n if (!pptSerializer) {\n return { err: new Errors.PPTSerializerInvalidError() };\n }\n\n try {\n decodedPayload = pptSerializer.decode(pptPayload as string | ArrayBuffer | Uint8Array) as { args?: unknown[]; kwargs?: Record<string, unknown> };\n } catch {\n return { err: new Errors.PPTSerializationError() };\n }\n } else {\n decodedPayload = pptPayload as { args?: unknown[]; kwargs?: Record<string, unknown> };\n }\n return { err: false, args: decodedPayload.args, kwargs: decodedPayload.kwargs };\n }\n\n /** Encode WAMP message */\n private _encode (msg: unknown[]): string | ArrayBuffer | Uint8Array | undefined {\n try {\n return this._options.serializer.encode(msg);\n } catch {\n this._hardClose('wamp.error.protocol_violation', 'Can not encode message', true);\n }\n }\n\n /** Decode WAMP message */\n private _decode (msg: unknown): unknown[] {\n try {\n return this._options.serializer.decode(msg as string | ArrayBuffer | Uint8Array) as unknown[];\n } catch {\n this._hardClose('wamp.error.protocol_violation', 'Can not decode received message');\n return [];\n }\n }\n\n /** Hard close of connection due to protocol violations */\n private _hardClose (errorUri: string, details: string, noSend: boolean = false): void {\n this._log(details);\n // Cleanup outgoing message queue\n this._wsQueue = [];\n\n if (!noSend) {\n this._send([WAMP_MSG_SPEC.ABORT, { message: details }, errorUri]);\n }\n\n const protocolViolationError = new Errors.ProtocolViolationError(errorUri, details);\n\n // In case we were just making first connection\n if (this._cache.connectPromise) {\n this._cache.connectPromise.onError(protocolViolationError);\n this._cache.connectPromise = null;\n }\n\n if (this._options.onError) {\n this._options.onError(protocolViolationError);\n }\n\n this._ws!.close();\n }\n\n /** Send encoded message to server */\n private _send (msg?: unknown[]): void {\n if (msg) {\n this._wsQueue.push(this._encode(msg));\n }\n\n if (this._ws && this._ws.readyState === 1 && this._cache.sessionId) {\n while (this._wsQueue.length > 0) {\n this._ws.send(this._wsQueue.shift() as string | ArrayBuffer);\n }\n }\n }\n\n /** Reject (fail) all ongoing promises on connection closing */\n private async _reject_ongoing_promises (error: Error): Promise<void> {\n const promises: (void | Promise<void>)[] = [];\n\n for (const call of Object.values(this._calls)) {\n if (call.onError) {\n promises.push(call.onError(error));\n }\n }\n for (const req of Object.values(this._requests)) {\n if (req.callbacks?.onError) {\n promises.push(req.callbacks.onError(error));\n }\n }\n\n await Promise.allSettled(promises);\n this._requests = {};\n this._calls = {};\n }\n\n /** Reset internal state and cache */\n private _resetState (): void {\n this._wsQueue = [];\n this._subscriptionsById.clear();\n this._subscriptionsByKey.clear();\n this._requests = {};\n this._calls = {};\n this._rpcRegs = {};\n this._rpcNames = new Set();\n\n // Just keep attrs that are have to be present\n this._cache = {\n reqId : 0,\n reconnectingAttempts: 0,\n opStatus : SUCCESS,\n closePromise : null,\n connectPromise : null,\n } as WampyCache;\n }\n\n /** Initialize internal websocket callbacks */\n private _initWsCallbacks (): void {\n this._ws!.onopen = () => this._wsOnOpen();\n this._ws!.onclose = async (event: CloseEvent) => this._wsOnClose(event);\n this._ws!.onmessage = (event: MessageEvent) => this._wsOnMessage(event);\n this._ws!.onerror = async (error: Event) => this._wsOnError(error);\n }\n\n /** Internal websocket on open callback */\n private _wsOnOpen (): void {\n const { helloCustomDetails, authmethods, authid, authextra, serializer, onError, realm } = this._options;\n const serverProtocol = this._ws!.protocol?.split('.')?.[2];\n const hasServerChosenOurPreferredProtocol = serverProtocol === serializer.protocol;\n\n this._log(`Websocket connected. Server has chosen protocol: \"${serverProtocol}\"`);\n\n if (!hasServerChosenOurPreferredProtocol) {\n if (serverProtocol === 'json') {\n this._options.serializer = new JsonSerializer();\n } else {\n const noSerializerAvailableError = new Errors.NoSerializerAvailableError();\n this._fillOpStatusByError(noSerializerAvailableError);\n\n if (this._cache.connectPromise) {\n this._cache.connectPromise.onError(noSerializerAvailableError);\n this._cache.connectPromise = null;\n }\n\n if (onError) {\n onError(noSerializerAvailableError);\n }\n }\n }\n\n if (serializer.isBinary) {\n this._ws!.binaryType = 'arraybuffer';\n }\n\n const messageOptions = {\n ...helloCustomDetails,\n ...this._wamp_features,\n ...(authid ? { authid, authmethods, authextra } : {}),\n };\n const encodedMessage = this._encode([WAMP_MSG_SPEC.HELLO, realm, messageOptions]);\n\n if (encodedMessage) {\n // Sending directly 'cause it's a hello message and no sessionId check is needed\n this._ws!.send(encodedMessage as string | ArrayBuffer);\n }\n }\n\n /** Internal websocket on close callback */\n async _wsOnClose (event: CloseEvent): Promise<void> {\n this._log('websocket disconnected. Info: ', event);\n\n await this._reject_ongoing_promises(new WebsocketError('Connection closed'));\n\n // Automatic reconnection\n if ((this._cache.sessionId || this._cache.reconnectingAttempts) &&\n this._options.autoReconnect &&\n (this._options.maxRetries === 0 ||\n this._cache.reconnectingAttempts < this._options.maxRetries) &&\n !this._cache.isSayingGoodbye) {\n this._cache.sessionId = null;\n this._cache.timer = setTimeout(() => {\n this._wsReconnect();\n }, this._options.reconnectInterval);\n } else {\n // No reconnection needed or reached max retries count\n if (this._options.onClose) {\n this._options.onClose();\n }\n if (this._cache.closePromise) {\n this._cache.closePromise.onSuccess(undefined as never);\n this._cache.closePromise = null;\n }\n this._resetState();\n this._ws = null;\n }\n }\n\n /** Internal websocket on event callback */\n async _wsOnMessage (event: MessageEvent): Promise<void> {\n const data = this._decode(event.data);\n\n this._log('websocket message received: ', data);\n\n const messageType = data[0] as number;\n const messageHandlers: Record<number, () => void | Promise<void>> = {\n [WAMP_MSG_SPEC.WELCOME]: () => this._onWelcomeMessage(data as [unknown, number, ServerWampFeatures]),\n [WAMP_MSG_SPEC.ABORT]: () => this._onAbortMessage(data as [unknown, Record<string, unknown>, string]),\n [WAMP_MSG_SPEC.CHALLENGE]: () => this._onChallengeMessage(data as [unknown, string, Record<string, unknown>]),\n [WAMP_MSG_SPEC.GOODBYE]: () => this._onGoodbyeMessage(),\n [WAMP_MSG_SPEC.ERROR]: () => this._onErrorMessage(data as [unknown, number, number, Record<string, unknown>, string, unknown[]?, Record<string, unknown>?]),\n [WAMP_MSG_SPEC.SUBSCRIBED]: () => this._onSubscribedMessage(data as [unknown, number, number]),\n [WAMP_MSG_SPEC.UNSUBSCRIBED]: () => this._onUnsubscribedMessage(data as [unknown, number]),\n [WAMP_MSG_SPEC.PUBLISHED]: () => this._onPublishedMessage(data as [unknown, number, number]),\n [WAMP_MSG_SPEC.EVENT]: () => this._onEventMessage(data as [unknown, number, number, Record<string, unknown>, unknown[]?, Record<string, unknown>?]),\n [WAMP_MSG_SPEC.RESULT]: () => this._onResultMessage(data as [unknown, number, Record<string, unknown>, unknown[]?, Record<string, unknown>?]),\n // [WAMP_MSG_SPEC.REGISTER]: () => {},\n [WAMP_MSG_SPEC.REGISTERED]: () => this._onRegisteredMessage(data as [unknown, number, number]),\n // [WAMP_MSG_SPEC.UNREGISTER]: () => {},\n [WAMP_MSG_SPEC.UNREGISTERED]: () => this._onUnregisteredMessage(data as [unknown, number]),\n [WAMP_MSG_SPEC.INVOCATION]: () => this._onInvocationMessage(data as [unknown, number, number, Record<string, unknown>, unknown[]?, Record<string, unknown>?]),\n // [WAMP_MSG_SPEC.INTERRUPT]: () => {},\n // [WAMP_MSG_SPEC.YIELD]: () => {},\n };\n const handler = messageHandlers[messageType];\n const errorURI = 'wamp.error.protocol_violation';\n\n if (!handler) {\n return this._hardClose(errorURI, `Received non-compliant WAMP message: \"${messageType}\"`);\n }\n\n const needNoSession = ([WAMP_MSG_SPEC.WELCOME, WAMP_MSG_SPEC.CHALLENGE] as number[]).includes(messageType);\n const needValidSession = !needNoSession && messageType !== WAMP_MSG_SPEC.ABORT;\n\n if (needNoSession && this._cache.sessionId) {\n return this._hardClose(errorURI, `Received message \"${messageType}\" after session was established`);\n }\n\n if (needValidSession && !this._cache.sessionId) {\n return this._hardClose(errorURI, `Received message \"${messageType}\" before session was established`);\n }\n\n if (this._isRequestIdValid(data)) {\n await handler();\n }\n }\n\n /** Validates the requestId for message types that need this kind of validation */\n _isRequestIdValid ([messageType, requestId]: unknown[]): boolean {\n const isRequestIdValidationNeeded = ([\n WAMP_MSG_SPEC.SUBSCRIBED,\n WAMP_MSG_SPEC.UNSUBSCRIBED,\n WAMP_MSG_SPEC.PUBLISHED,\n WAMP_MSG_SPEC.RESULT,\n WAMP_MSG_SPEC.REGISTERED,\n WAMP_MSG_SPEC.UNREGISTERED\n ] as number[]).includes(messageType as number);\n\n if (!isRequestIdValidationNeeded) {\n return true;\n }\n\n if (messageType === WAMP_MSG_SPEC.RESULT && this._calls[requestId as number]) {\n return true;\n }\n\n if (this._requests[requestId as number]) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Handles websocket welcome message event\n * WAMP SPEC: [WELCOME, Session|id, Details|dict]\n */\n async _onWelcomeMessage ([, sessionId, details]: [unknown, number, ServerWampFeatures]): Promise<void> {\n this._cache.sessionId = sessionId;\n this._cache.server_wamp_features = details;\n\n if (this._cache.reconnectingAttempts) {\n this._cache.reconnectingAttempts = 0;\n\n if (this._options.onReconnectSuccess) {\n await this._options.onReconnectSuccess(details as unknown as Record<string, unknown>);\n }\n\n // Renew all previous state\n await Promise.allSettled([this._renewSubscriptions(), this._renewRegistrations()]);\n } else {\n // Fire onConnect event on real connection to WAMP server\n this._cache.connectPromise!.onSuccess(details as unknown as Record<string, unknown>);\n this._cache.connectPromise = null;\n }\n\n // Send local queue if there is something out there\n this._send();\n }\n\n /**\n * Handles websocket abort message event\n * WAMP SPEC: [ABORT, Details|dict, Error|uri]\n */\n async _onAbortMessage ([, details, error]: [unknown, Record<string, unknown>, string]): Promise<void> {\n const err = new Errors.AbortError({ error, details });\n if (this._cache.connectPromise) {\n this._cache.connectPromise.onError(err);\n this._cache.connectPromise = null;\n }\n if (this._options.onError) {\n await this._options.onError(err);\n }\n this._ws!.close();\n }\n\n /**\n * Handles websocket challenge message event\n * WAMP SPEC: [CHALLENGE, AuthMethod|string, Extra|dict]\n */\n async _onChallengeMessage ([, authMethod, extra]: [unknown, string, Record<string, unknown>]): Promise<void> {\n let promise: Promise<string>;\n\n const { authid, authMode, onChallenge, onError, authPlugins } = this._options;\n\n if (authid && authMode === 'manual' && typeof onChallenge === 'function') {\n promise = new Promise((resolve) => {\n resolve(onChallenge(authMethod, extra));\n });\n } else if (authid && authMode === 'auto' && typeof authPlugins[authMethod] === 'function') {\n promise = new Promise((resolve) => {\n resolve(authPlugins[authMethod](authMethod, extra));\n });\n } else {\n const noCRACallbackOrIdError = new Errors.NoCRACallbackOrIdError();\n\n this._fillOpStatusByError(noCRACallbackOrIdError);\n this._ws!.send(this._encode([\n WAMP_MSG_SPEC.ABORT,\n { message: noCRACallbackOrIdError.message },\n 'wamp.error.cannot_authenticate'\n ]) as string | ArrayBuffer);\n\n if (onError) {\n await onError(noCRACallbackOrIdError);\n }\n\n return this._ws!.close() as unknown as void;\n }\n\n try {\n const key = await promise;\n\n // Sending directly 'cause it's a challenge msg and no sessionId check is needed\n this._ws!.send(this._encode([WAMP_MSG_SPEC.AUTHENTICATE, key, {}]) as string | ArrayBuffer);\n } catch {\n const challengeExceptionError = new Errors.ChallengeExceptionError();\n\n this._fillOpStatusByError(challengeExceptionError);\n this._ws!.send(this._encode([\n WAMP_MSG_SPEC.ABORT,\n { message: challengeExceptionError.message },\n 'wamp.error.cannot_authenticate'\n ]) as string | ArrayBuffer);\n\n if (onError) {\n await onError(challengeExceptionError);\n }\n\n this._ws!.close();\n }\n }\n\n /**\n * Handles websocket goodbye message event\n * WAMP SPEC: [GOODBYE, Details|dict, Reason|uri]\n */\n async _onGoodbyeMessage (): Promise<void> {\n if (!this._cache.isSayingGoodbye) { // get goodbye, initiated by server\n this._cache.isSayingGoodbye = true;\n this._send([WAMP_MSG_SPEC.GOODBYE, {}, 'wamp.close.goodbye_and_out']);\n }\n this._cache.sessionId = null;\n this._ws!.close();\n }\n\n /**\n * Handles websocket error message event\n * WAMP SPEC: [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict,\n * Error|uri, (Arguments|list, ArgumentsKw|dict)]\n */\n async _onErrorMessage ([, requestType, requestId, details, error, argsList, argsDict]: [unknown, number, number, Record<string, unknown>, string, unknown[]?, Record<string, unknown>?]): Promise<void> {\n const errorOptions = { error, details, argsList, argsDict };\n const errorsByRequestType: Record<number, Error> = {\n [WAMP_MSG_SPEC.SUBSCRIBE]: new Errors.SubscribeError(errorOptions),\n [WAMP_MSG_SPEC.UNSUBSCRIBE]: new Errors.UnsubscribeError(errorOptions),\n [WAMP_MSG_SPEC.PUBLISH]: new Errors.PublishError(errorOptions),\n [WAMP_MSG_SPEC.REGISTER]: new Errors.RegisterError(errorOptions),\n [WAMP_MSG_SPEC.UNREGISTER]: new Errors.UnregisterError(errorOptions),\n // [WAMP_MSG_SPEC.INVOCATION]:\n [WAMP_MSG_SPEC.CALL]: new Errors.CallError(errorOptions),\n };\n const currentError = errorsByRequestType[requestType];\n\n if (!currentError) {\n return this._hardClose('wamp.error.protocol_violation', 'Received invalid ERROR message');\n }\n\n if (requestType === WAMP_MSG_SPEC.CALL) {\n const call = this._calls[requestId];\n if (call?.onError) {\n await call.onError(currentError);\n }\n delete this._calls[requestId];\n } else {\n const req = this._requests[requestId];\n if (req?.callbacks?.onError) {\n await req.callbacks.onError(currentError);\n }\n delete this._requests[requestId];\n }\n }\n\n /**\n * Handles websocket subscribed message event\n * WAMP SPEC: [SUBSCRIBED, SUBSCRIBE.Request|id, Subscription|id]\n */\n async _onSubscribedMessage ([, requestId, subscriptionId]: [unknown, number, number]): Promise<void> {\n const { topic, advancedOptions, callbacks } = this._requests[requestId] as SubscribeRequest;\n const subscription: SubscriptionCallbacksHash = {\n id: subscriptionId,\n topic,\n advancedOptions,\n callbacks: [callbacks.onEvent]\n };\n const subscriptionKey = this._getSubscriptionKey(topic, advancedOptions);\n\n this._subscriptionsById.set(subscriptionId, subscription);\n this._subscriptionsByKey.set(subscriptionKey, subscription);\n\n if (callbacks.onSuccess) {\n await callbacks.onSuccess({ topic, requestId, subscriptionId, subscriptionKey });\n }\n\n delete this._requests[requestId];\n }\n\n /**\n * Handles websocket unsubscribed message event\n * WAMP SPEC: [UNSUBSCRIBED, UNSUBSCRIBE.Request|id]\n */\n async _onUnsubscribedMessage ([, requestId]: [unknown, number]): Promise<void> {\n const { topic, advancedOptions, callbacks } = this._requests[requestId] as UnsubscribeRequest;\n const subscriptionKey = this._getSubscriptionKey(topic, advancedOptions);\n const subscriptionId = this._subscriptionsByKey.get(subscriptionKey)!.id;\n this._subscriptionsByKey.delete(subscriptionKey);\n this._subscriptionsById.delete(subscriptionId);\n\n if (callbacks.onSuccess) {\n await callbacks.onSuccess({ topic, requestId });\n }\n\n delete this._requests[requestId];\n }\n\n /**\n * Handles websocket published message event\n * WAMP SPEC: [PUBLISHED, PUBLISH.Request|id, Publication|id]\n */\n async _onPublishedMessage ([, requestId, publicationId]: [unknown, number, number]): Promise<void> {\n const { topic, callbacks } = this._requests[requestId] as PublishRequest;\n\n if (callbacks?.onSuccess) {\n await callbacks.onSuccess({ topic, requestId, publicationId });\n }\n\n delete this._requests[requestId];\n }\n\n /**\n * Handles websocket event message event\n * WAMP SPEC: [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id,\n * Details|dict, PUBLISH.Arguments|list, PUBLISH.ArgumentKw|dict]\n */\n async _onEventMessage ([, subscriptionId, publicationId, details, argsList, argsDict]: [unknown, number, number, Record<string, unknown>, unknown[]?, Record<string, unknown>?]): Promise<void> {\n const subscription = this._subscriptionsById.get(subscriptionId);\n\n if (!subscription) {\n return;\n }\n\n let args = argsList;\n let kwargs = argsDict;\n\n // Check and handle Payload PassThru Mode\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-passthru-mode\n if (details.ppt_scheme) {\n const pptPayload = argsList![0];\n const decodedPayload = this._unpackPPTPayload('broker', pptPayload, details as Record<string, unknown>);\n\n if (decodedPayload.err) {\n // Since it is async publication, and no link to\n // original publication - as it was already published\n // we can not reply with error, only log it.\n // Although the router should handle it\n return this._log((decodedPayload.err as Error).message);\n }\n\n args = decodedPayload.args;\n kwargs = decodedPayload.kwargs;\n }\n\n const callbackOptions = { details: details as Record<string, unknown>, argsList: args, argsDict: kwargs };\n const callbackPromises = subscription.callbacks.map((c) => c(callbackOptions));\n\n await Promise.all(callbackPromises);\n }\n\n /**\n * Handles websocket result message event\n * WAMP SPEC: [RESULT, CALL.Request|id, Details|dict,\n * YIELD.Arguments|list, YIELD.ArgumentsKw|dict]\n */\n async _onResultMessage ([, requestId, details, argsList, argsDict]: [unknown, number, Record<string, unknown>, unknown[]?, Record<string, unknown>?]): Promise<void> {\n let args = argsList;\n let kwargs = argsDict;\n\n // Check and handle Payload PassThru Mode\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-passthru-mode\n if (details.ppt_scheme) {\n const pptPayload = argsList![0];\n const decodedPayload = this._unpackPPTPayload('dealer', pptPayload, details as Record<string, unknown>);\n\n if (decodedPayload.err) {\n this._log((decodedPayload.err as Error).message);\n this._cache.opStatus = decodedPayload.err as unknown as WampyOpStatus;\n await this._calls[requestId].onError(new Errors.CallError({\n details: details as Record<string, unknown>,\n error : 'wamp.error.invocation_exception',\n argsList : [(decodedPayload.err as Error).message],\n argsDict : undefined\n }));\n delete this._calls[requestId];\n\n return;\n }\n\n args = decodedPayload.args;\n kwargs = decodedPayload.kwargs;\n }\n\n const callbackOptions: CallResult = { details: details as Record<string, unknown>, argsList: args, argsDict: kwargs };\n\n if (details.progress) {\n await this._calls[requestId].onProgress!(callbackOptions);\n } else {\n // We received final result (progressive or not)\n await this._calls[requestId].onSuccess(callbackOptions);\n delete this._calls[requestId];\n }\n }\n\n /**\n * Handles websocket registered message event\n * WAMP SPEC: [REGISTERED, REGISTER.Request|id, Registration|id]\n */\n async _onRegisteredMessage ([, requestId, registrationId]: [unknown, number, number]): Promise<void> {\n const { topic, callbacks, options } = this._requests[requestId] as RegisterRequest;\n\n this._rpcRegs[registrationId] = { id: registrationId, callbacks: [callbacks.rpc], options };\n this._rpcRegs[topic] = this._rpcRegs[registrationId];\n this._rpcNames.add(topic);\n\n if (callbacks?.onSuccess) {\n await callbacks.onSuccess({ topic, requestId, registrationId });\n }\n\n delete this._requests[requestId];\n }\n\n /**\n * Handles websocket unregistered message event\n * WAMP SPEC: [UNREGISTERED, UNREGISTER.Request|id]\n */\n async _onUnregisteredMessage ([, requestId]: [unknown, number]): Promise<void> {\n const { topic, callbacks } = this._requests[requestId] as UnregisterRequest;\n\n delete this._rpcRegs[this._rpcRegs[topic].id];\n delete this._rpcRegs[topic];\n\n if (this._rpcNames.has(topic)) {\n this._rpcNames.delete(topic);\n }\n\n if (callbacks?.onSuccess) {\n await callbacks.onSuccess({ topic, requestId });\n }\n\n delete this._requests[requestId];\n }\n\n /**\n * Handles websocket invocation message event\n * WAMP SPEC: [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict,\n * CALL.Arguments|list, CALL.ArgumentsKw|dict]\n */\n async _onInvocationMessage ([, requestId, registrationId, details, argsList, argsDict]: [unknown, number, number, Record<string, unknown>, unknown[]?, Record<string, unknown>?]): Promise<void> {\n const self = this;\n const handleInvocationError = ({ error, details, argsList, argsDict }: InvocationErrorData): void => {\n const message: unknown[] = [\n WAMP_MSG_SPEC.ERROR,\n WAMP_MSG_SPEC.INVOCATION,\n requestId,\n details || {},\n error || 'wamp.error.invocation_exception',\n ];\n\n if (Array.isArray(argsList)) {\n message.push(argsList);\n }\n\n if (self._isPlainObject(argsDict)) {\n if (!Array.isArray(argsList)) {\n message.push([]);\n }\n message.push(argsDict);\n }\n\n self._send(message);\n };\n\n if (!this._rpcRegs[registrationId]) {\n this._log(WAMP_ERROR_MSG.NON_EXIST_RPC_INVOCATION);\n return handleInvocationError({ error: 'wamp.error.no_such_procedure' });\n }\n\n let args = argsList;\n let kwargs = argsDict;\n\n // Check and handle Payload PassThru Mode\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-passthru-mode\n if (details?.ppt_scheme) {\n const pptPayload = argsList![0];\n const decodedPayload = this._unpackPPTPayload('dealer', pptPayload, details as Record<string, unknown>);\n\n // This case should not happen at all, but for safety\n if (decodedPayload.err) {\n this._log((decodedPayload.err as Error).message);\n\n if (decodedPayload.err instanceof Errors.PPTNotSupportedError) {\n // This case should not happen at all, but for safety\n return this._hardClose('wamp.error.protocol_violation',\n 'Received INVOCATION in PPT Mode, while Dealer didn\\'t announce it');\n }\n\n return handleInvocationError({\n details: details as Record<string, unknown>,\n error: 'wamp.error.invocation_exception',\n argsList: [(decodedPayload.err as Error).message],\n });\n }\n\n args = decodedPayload.args;\n kwargs = decodedPayload.kwargs;\n }\n\n const handleInvocationResult = (result: InvocationResult | null | void): void => {\n const options = result?.options || {};\n const { ppt_scheme, ppt_serializer, ppt_cipher, ppt_keyid } = options;\n\n // Check and handle Payload PassThru Mode\n // @see https://wamp-proto.org/wamp_latest_ietf.html#name-payload-passthru-mode\n if (ppt_scheme && !this._checkPPTOptions('dealer', options as Record<string, unknown>)) {\n if (this._cache.opStatus.error instanceof Errors.PPTNotSupportedError) {\n // This case should not happen at all, but for safety\n return this._hardClose('wamp.error.protocol_violation',\n 'Trying to send YIELD in PPT Mode, while Dealer didn\\'t announce it');\n }\n\n return handleInvocationError({\n details : options as Record<string, unknown>,\n error : 'wamp.error.invalid_option',\n argsList: [this._cache.opStatus.error!.message],\n });\n }\n\n const { err, payloadItems } = result ? this._packPPTPayload(result as unknown as Payload, options as Record<string, unknown>) : {} as Partial<PackPPTPayloadResult>;\n\n if (err) {\n return handleInvocationError({\n details : options as Record<string, unknown>,\n error : 'wamp.error.invocation_exception',\n argsList: [this._cache.opStatus.error!.message],\n });\n }\n\n const messageOptions: Record<string, unknown> = {\n ...options,\n ...(ppt_scheme ? { ppt_scheme } : {}),\n ...(ppt_serializer ? { ppt_serializer } : {}),\n ...(ppt_cipher ? { ppt_cipher } : {}),\n ...(ppt_keyid ? { ppt_keyid } : {}),\n ...this._extractCustomOptions(options as Record<string, unknown>)\n };\n\n // WAMP SPEC: [YIELD, INVOCATION.Request|id, Options|dict, Arguments|list, ArgumentsKw|dict]\n self._send([WAMP_MSG_SPEC.YIELD, requestId, messageOptions, ...(payloadItems || [])]);\n };\n\n try {\n const result = await this._rpcRegs[registrationId].callbacks[0]({\n details: details as Record<string, unknown>,\n argsList : args,\n argsDict : kwargs,\n result_handler: handleInvocationResult,\n error_handler : handleInvocationError\n });\n handleInvocationResult(result);\n } catch (e) {\n handleInvocationError(e as InvocationErrorData);\n }\n }\n\n /** Internal websocket on error callback */\n async _wsOnError (error: Event): Promise<void> {\n this._log('websocket error');\n const websocketError = new Errors.WebsocketError(error);\n\n await this._reject_ongoing_promises(websocketError);\n\n if (this._cache.connectPromise) {\n this._cache.connectPromise.onError(websocketError);\n this._cache.connectPromise = null;\n }\n\n if (this._options.onError) {\n this._options.onError(websocketError);\n }\n }\n\n /** Reconnect to server in case of websocket error */\n _wsReconnect (): void {\n this._log('websocket reconnecting...');\n\n if (this._options.onReconnect) {\n this._options.onReconnect();\n }\n\n this._cache.reconnectingAttempts++;\n this._ws = getWebSocket({\n url: this._url!,\n protocols: this._protocols,\n options: this._options as Record<string, unknown>\n });\n this._initWsCallbacks();\n }\n\n /** Resubscribe to topics in case of communication error */\n async _renewSubscriptions (): Promise<void> {\n let i: number;\n const subs = new Map(this._subscriptionsById);\n\n this._subscriptionsById.clear();\n this._subscriptionsByKey.clear();\n\n for (const sub of subs.values()) {\n i = sub.callbacks.length;\n while (i--) {\n try {\n await this.subscribe(sub.topic, sub.callbacks[i], sub.advancedOptions);\n } catch (err) {\n this._log(`cannot resubscribe to topic: ${sub.topic}`, err);\n\n if (this._options.onError) {\n this._options.onError(err as Error);\n }\n }\n }\n }\n }\n\n /** ReRegister RPCs in case of communication error */\n async _renewRegistrations (): Promise<void> {\n const rpcs = this._rpcRegs,\n rn = this._rpcNames;\n\n this._rpcRegs = {};\n this._rpcNames = new Set();\n\n for (const rpcName of rn) {\n try {\n await this.register(rpcName, rpcs[rpcName].callbacks[0], rpcs[rpcName].options);\n } catch (err) {\n this._log(`cannot renew registration of rpc: ${rpcName}`, err);\n\n if (this._options.onError) {\n this._options.onError(err as Error);\n }\n }\n }\n }\n\n /**\n * Generate a unique key for combination of topic and options\n *\n * This is needed to allow subscriptions to the same topic URI but with different options\n */\n _getSubscriptionKey (topic: string, options?: SubscribeAdvancedOptions): string {\n return `${topic}${options ? `-${JSON.stringify(options)}` : ''}`;\n }\n\n /*************************************************************************\n * Wampy public API\n *************************************************************************/\n\n /** Wampy options getter */\n getOptions (): Required<WampyOptions> {\n return this._options;\n }\n\n /** Wampy options setter */\n setOptions (newOptions: WampyOptions): Wampy | undefined {\n if (this._isPlainObject(newOptions)) {\n this._options = { ...this._options, ...newOptions as WampyOptions };\n return this;\n }\n }\n\n /**\n * Get the status of last operation\n *\n * Returns an object with 3 fields: code, error, reqId\n * code: 0 - if operation was successful\n * code > 0 - if error occurred\n * error: error instance containing details\n * reqId: last successfully sent request ID\n */\n getOpStatus (): WampyOpStatus {\n return this._cache.opStatus;\n }\n\n /** Get the WAMP Session ID */\n getSessionId (): number | null {\n