UNPKG

wampy

Version:

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

1,500 lines (1,491 loc) 84 kB
#!/usr/bin/env node // cmd/main.ts import { hideBin } from "yargs/helpers"; import yargs from "yargs/yargs"; // cmd/commands/call.ts import cj from "color-json"; // cmd/common-options.ts function convertStringToBoolean(obj) { for (const key in obj) { if (typeof obj[key] === "object" && obj[key] !== null) { obj[key] = convertStringToBoolean(obj[key]); } else if (typeof obj[key] === "string" || Object.prototype.toString.call(obj[key]) === "[object String]") { if (obj[key].toUpperCase() === "TRUE") { obj[key] = true; } else if (obj[key].toUpperCase() === "FALSE") { obj[key] = false; } } } return obj; } var payloadArgs = function(yargs2) { return yargs2.option("strbool", { alias: "b", description: 'Treat payload strings "true", "false" as boolean', type: "boolean", default: false }).option("json", { alias: "j", description: "Treat payload as json-encoded strings and decode them before sending", type: "boolean", default: false }).option("argsList", { alias: ["a", "args"], description: 'Message positional (list) payload\nYou can pass multiple values after key:\n--argsList 1 2 3 ==> [1, 2, 3]\n-a 100 string ==> [100, "string"]', type: "array" }).option("argsDict", { alias: ["k", "kwargs"], description: 'Message Key-value (dictionary) payload\nTo specify values use dot notation (any level deep):\n--argsDict.key1 125 ==> { key1: 125}\n-k.key1 250 -k.key2 my-string ==>\n{ key1: 250, key2: "my-string" }\n-k.rootKey true -k.innerObj.key1 cool ==>\n{ rootKey: true, innerObj: { key1: "cool" }}\n' }).middleware((argv2) => { if (!argv2.strbool) { return; } if (argv2.argsList) { argv2.argsList = argv2.argsList.map((v) => { if (typeof v === "string" || Object.prototype.toString.call(v) === "[object String]") { if (v.toUpperCase() === "TRUE") { return true; } else if (v.toUpperCase() === "FALSE") { return false; } return v; } return v; }); } if (argv2.argsDict) { argv2.argsDict = convertStringToBoolean(argv2.argsDict); } }).middleware((argv2) => { if (!argv2.json) { return; } if (argv2.argsList) { argv2.argsList = argv2.argsList.map((v) => { if (typeof v === "string" || Object.prototype.toString.call(v) === "[object String]") { return JSON.parse(v); } return v; }); } if (argv2.argsDict && (typeof argv2.argsDict === "string" || Object.prototype.toString.call(argv2.argsDict) === "[object String]")) { argv2.argsDict = JSON.parse(argv2.argsDict); } }).group(["strbool", "json", "argsList", "argsDict"], "Payload options:"); }; var pptArgs = function(yargs2) { return yargs2.option("ppt_scheme", { description: "Identifies the Payload Schema for Payload Passthru Mode", type: "string" }).option("ppt_serializer", { description: "Specifies what serializer was used to encode the payload", type: "string" }).option("ppt_cipher", { description: "Specifies the cryptographic algorithm that was used to encrypt the payload", type: "string" }).option("ppt_keyid", { description: "Contains the encryption key id that was used to encrypt the payload", type: "string" }).group(["ppt_scheme", "ppt_serializer", "ppt_cipher", "ppt_keyid"], "Payload Passthru Mode options:"); }; var helpOptions = function(yargs2) { return yargs2.help().alias("help", "h").showHelpOnFail(false, "Specify -h (--help) for available options"); }; var connOptsKeys = [ "url", "realm", "authid", "secret", "ticket", "privateKey", "noReconnect", "reconnectInterval", "maxRetries", "helloCustomDetails" ]; var connOptions = function(yargs2) { return yargs2.option("url", { alias: "w", description: "WAMP Router Endpoint URL", type: "string", demandOption: true }).option("realm", { alias: "r", description: "WAMP Realm to join on server", type: "string", demandOption: true }).option("authid", { alias: "u", description: "Authentication (user) id to use in challenge", type: "string" }).option("ticket", { alias: ["password"], description: "Ticket (Password) for the Ticket Authentication methods", type: "string" }).option("secret", { alias: ["sc"], description: "Secret (Password) for the CRA Authentication methods", type: "string" }).option("privateKey", { alias: ["pk"], description: "Hex-encoded Private Key for Cryptosign Authentication method", type: "string" }).implies("ticket", "authid").implies("secret", "authid").implies("privateKey", "authid").option("noReconnect", { alias: "nr", description: "Disable auto reconnecting", type: "boolean", default: false }).option("reconnectInterval", { alias: "ri", description: "Reconnect Interval (in ms)", type: "number", default: 2e3 }).option("maxRetries", { alias: "mr", description: "Maximum Retries count", type: "number", default: 25 }).option("helloCustomDetails", { alias: "hello", description: 'Custom attributes (Key-value) to send to router on hello\nTo specify values use dot notation (any level deep):\n--hello.key1 250 --hello.key2 my-string ==>\n{ key1: 250, key2: "my-string" }\n--hello.rootKey value1 --hello.innerObj.key1 cool ==>\n{ rootKey: "value1", innerObj: { key1: "cool" }}' }).global(connOptsKeys).group(connOptsKeys, "Connection options:"); }; // src/constants.ts var WAMP_MSG_SPEC = { HELLO: 1, WELCOME: 2, ABORT: 3, CHALLENGE: 4, AUTHENTICATE: 5, GOODBYE: 6, ERROR: 8, PUBLISH: 16, PUBLISHED: 17, SUBSCRIBE: 32, SUBSCRIBED: 33, UNSUBSCRIBE: 34, UNSUBSCRIBED: 35, EVENT: 36, CALL: 48, CANCEL: 49, RESULT: 50, REGISTER: 64, REGISTERED: 65, UNREGISTER: 66, UNREGISTERED: 67, INVOCATION: 68, INTERRUPT: 69, YIELD: 70 }; var SUCCESS = { code: 0, error: null }; var WAMP_ERROR_MSG = { SUCCESS: "Success!", URI_ERROR: "Topic URI doesn't meet requirements!", NO_BROKER: "Server doesn't provide broker role!", NO_CALLBACK_SPEC: "No required callback function specified!", INVALID_PARAM: "Invalid parameter(s) specified!", NO_SERIALIZER_AVAILABLE: "Server has chosen a serializer, which is not available!", NON_EXIST_UNSUBSCRIBE: "Trying to unsubscribe from non existent subscription!", NO_DEALER: "Server doesn't provide dealer role!", RPC_ALREADY_REGISTERED: "RPC already registered!", NON_EXIST_RPC_UNREG: "Received rpc unregistration for non existent rpc!", NON_EXIST_RPC_INVOCATION: "Received invocation for non existent rpc!", NON_EXIST_RPC_REQ_ID: "No RPC calls in action with specified request ID!", NO_REALM: "No realm specified!", NO_WS_OR_URL: "No websocket provided or URL specified is incorrect!", NO_CRA_CB_OR_ID: "No onChallenge callback or authid was provided for authentication!", CHALLENGE_EXCEPTION: "Exception raised during challenge processing", PPT_NOT_SUPPORTED: "Payload Passthru Mode is not supported by the router", PPT_INVALID_SCHEME: "Provided PPT scheme is invalid", PPT_SRLZ_INVALID: "Provided PPT serializer is invalid or not supported", PPT_SRLZ_ERR: "Can not serialize/deserialize payload", PROTOCOL_VIOLATION: "Protocol violation", WAMP_ABORT: "Router aborted connection", WAMP_GENERAL_ERROR: "Wamp error", WEBSOCKET_ERROR: "Websocket error", FEATURE_NOT_SUPPORTED: "Feature not supported" }; var E2EE_SERIALIZERS = ["cbor"]; var isNode = typeof process === "object" && Object.prototype.toString.call(process) === "[object process]"; var WAMP_CUSTOM_ATTR_REGEX = /^_[a-z0-9_]{3,}$/; // src/errors.ts var UriError = class extends Error { code = 1; constructor() { super(WAMP_ERROR_MSG.URI_ERROR); this.name = "UriError"; } }; var NoBrokerError = class extends Error { code = 2; constructor() { super(WAMP_ERROR_MSG.NO_BROKER); this.name = "NoBrokerError"; } }; var NoCallbackError = class extends Error { code = 3; constructor() { super(WAMP_ERROR_MSG.NO_CALLBACK_SPEC); this.name = "NoCallbackError"; } }; var InvalidParamError = class extends Error { code = 4; parameter; constructor(parameter) { super(WAMP_ERROR_MSG.INVALID_PARAM); this.name = "InvalidParamError"; this.parameter = parameter; } }; var NoSerializerAvailableError = class extends Error { code = 6; constructor() { super(WAMP_ERROR_MSG.NO_SERIALIZER_AVAILABLE); this.name = "NoSerializerAvailableError"; } }; var NonExistUnsubscribeError = class extends Error { code = 7; constructor() { super(WAMP_ERROR_MSG.NON_EXIST_UNSUBSCRIBE); this.name = "NonExistUnsubscribeError"; } }; var NoDealerError = class extends Error { code = 12; constructor() { super(WAMP_ERROR_MSG.NO_DEALER); this.name = "NoDealerError"; } }; var RPCAlreadyRegisteredError = class extends Error { code = 15; constructor() { super(WAMP_ERROR_MSG.RPC_ALREADY_REGISTERED); this.name = "RPCAlreadyRegisteredError"; } }; var NonExistRPCUnregistrationError = class extends Error { code = 17; constructor() { super(WAMP_ERROR_MSG.NON_EXIST_RPC_UNREG); this.name = "NonExistRPCUnregistrationError"; } }; var NonExistRPCReqIdError = class extends Error { code = 20; constructor() { super(WAMP_ERROR_MSG.NON_EXIST_RPC_REQ_ID); this.name = "NonExistRPCReqIdError"; } }; var NoRealmError = class extends Error { code = 21; constructor() { super(WAMP_ERROR_MSG.NO_REALM); this.name = "NoRealmError"; } }; var NoWsOrUrlError = class extends Error { code = 22; constructor() { super(WAMP_ERROR_MSG.NO_WS_OR_URL); this.name = "NoWsOrUrlError"; } }; var NoCRACallbackOrIdError = class extends Error { code = 23; errorUri = "wamp.error.cannot_authenticate"; constructor() { super(WAMP_ERROR_MSG.NO_CRA_CB_OR_ID); this.name = "NoCRACallbackOrIdError"; } }; var ChallengeExceptionError = class extends Error { code = 24; errorUri = "wamp.error.cannot_authenticate"; constructor() { super(WAMP_ERROR_MSG.CHALLENGE_EXCEPTION); this.name = "ChallengeExceptionError"; } }; var PPTNotSupportedError = class extends Error { code = 25; constructor() { super(WAMP_ERROR_MSG.PPT_NOT_SUPPORTED); this.name = "PPTNotSupportedError"; } }; var PPTInvalidSchemeError = class extends Error { code = 26; constructor() { super(WAMP_ERROR_MSG.PPT_INVALID_SCHEME); this.name = "PPTInvalidSchemeError"; } }; var PPTSerializerInvalidError = class extends Error { code = 27; constructor() { super(WAMP_ERROR_MSG.PPT_SRLZ_INVALID); this.name = "PPTSerializerInvalidError"; } }; var PPTSerializationError = class extends Error { code = 28; constructor() { super(WAMP_ERROR_MSG.PPT_SRLZ_ERR); this.name = "PPTSerializationError"; } }; var ProtocolViolationError = class extends Error { code = 29; errorUri; constructor(errorUri, details) { super(details || WAMP_ERROR_MSG.PROTOCOL_VIOLATION); this.name = "ProtocolViolationError"; this.errorUri = errorUri; } }; var AbortError = class extends Error { code = 30; errorUri; details; constructor({ error, details }) { super(WAMP_ERROR_MSG.WAMP_ABORT); this.name = "AbortedError"; this.errorUri = error; this.details = details; } }; var WampError = class extends Error { code = 31; errorUri; details; argsList; argsDict; constructor({ error, details, argsList, argsDict }) { super(WAMP_ERROR_MSG.WAMP_GENERAL_ERROR); this.name = "WampError"; this.errorUri = error; this.details = details; this.argsList = argsList; this.argsDict = argsDict; } }; var SubscribeError = class extends WampError { code = 32; constructor({ error, details, argsList, argsDict }) { super({ error, details, argsList, argsDict }); this.name = "SubscribeError"; } }; var UnsubscribeError = class extends WampError { code = 33; constructor({ error, details, argsList, argsDict }) { super({ error, details, argsList, argsDict }); this.name = "UnsubscribeError"; } }; var PublishError = class extends WampError { code = 34; constructor({ error, details, argsList, argsDict }) { super({ error, details, argsList, argsDict }); this.name = "PublishError"; } }; var RegisterError = class extends WampError { code = 35; constructor({ error, details, argsList, argsDict }) { super({ error, details, argsList, argsDict }); this.name = "RegisterError"; } }; var UnregisterError = class extends WampError { code = 36; constructor({ error, details, argsList, argsDict }) { super({ error, details, argsList, argsDict }); this.name = "UnregisterError"; } }; var CallError = class extends WampError { code = 37; constructor({ error, details, argsList, argsDict }) { super({ error, details, argsList, argsDict }); this.name = "CallError"; } }; var WebsocketError = class extends Error { code = 38; error; constructor(error) { super(WAMP_ERROR_MSG.WEBSOCKET_ERROR); this.name = "WebsocketError"; this.error = error; } }; var FeatureNotSupportedError = class extends Error { code = 39; role; feature; constructor(role, feature) { super(WAMP_ERROR_MSG.FEATURE_NOT_SUPPORTED); this.name = "FeatureNotSupportedError"; this.role = role; this.feature = feature; } }; // src/utils.ts function isWebSocketSchemeSpecified(url) { return /^ws(s)?:\/\//.test(url); } function getServerUrlForNode(url) { return isWebSocketSchemeSpecified(url) ? url : null; } function getServerUrlForBrowser(url) { if (url && isWebSocketSchemeSpecified(url)) { return url; } const isSecureProtocol = globalThis.location.protocol === "https:"; const scheme = isSecureProtocol ? "wss://" : "ws://"; const port = globalThis.location.port ? `:${globalThis.location.port}` : ""; if (!url) { return `${scheme}${globalThis.location.hostname}${port}/ws`; } if (url.startsWith("/")) { return `${scheme}${globalThis.location.hostname}${port}${url}`; } return `${scheme}${url}`; } function getWebSocketFromWindowObject(parsedUrl, protocols) { if (globalThis?.WebSocket) { return new globalThis.WebSocket(parsedUrl, protocols); } return null; } function getWebSocket({ url, protocols, options, isBrowserMock } = {}) { const { ws, additionalHeaders, wsRequestOptions } = options || {}; const isActualNode = isNode && !isBrowserMock; if (!ws && isActualNode) { return null; } const parsedUrl = isActualNode ? getServerUrlForNode(url) : getServerUrlForBrowser(url); if (!parsedUrl) { return null; } if (ws) { return new ws(parsedUrl, protocols, null, additionalHeaders, wsRequestOptions); } return getWebSocketFromWindowObject(parsedUrl, protocols); } function getNewPromise() { const deferred = {}; deferred.promise = new Promise(function(resolve, reject) { deferred.onSuccess = resolve; deferred.onError = reject; }); return deferred; } // src/serializers/json-serializer.ts var JsonSerializer = class { protocol = "json"; isBinary = false; encode(data) { return JSON.stringify(data); } decode(data) { return JSON.parse(data); } }; // src/wampy.ts var jsonSerializer = new JsonSerializer(); var Wampy = class { /** Wampy version */ version = "v8.0.0"; /** WS Url */ _url; /** WS protocols */ _protocols; /** WAMP features, supported by Wampy */ _wamp_features; /** Internal cache for object lifetime */ _cache; /** WebSocket object */ _ws; /** Internal queue for websocket requests, for case of disconnect */ _wsQueue; /** Internal queue for wamp requests */ _requests; /** Stored RPC */ _calls; /** Stored Pub/Subs to access by ID */ _subscriptionsById; /** Stored Pub/Subs to access by Key */ _subscriptionsByKey; /** Stored RPC Registrations */ _rpcRegs; /** Stored RPC names */ _rpcNames; /** Options hash-table */ _options; constructor(url, options) { this._url = typeof url === "string" ? url : null; this._protocols = ["wamp.2.json"]; this._wamp_features = { agent: "Wampy.js " + this.version, roles: { publisher: { features: { subscriber_blackwhite_listing: true, publisher_exclusion: true, publisher_identification: true, payload_passthru_mode: true } }, subscriber: { features: { pattern_based_subscription: true, publication_trustlevels: true, publisher_identification: true, payload_passthru_mode: true } }, caller: { features: { caller_identification: true, progressive_call_results: true, call_canceling: true, call_timeout: true, payload_passthru_mode: true } }, callee: { features: { caller_identification: true, call_trustlevels: true, pattern_based_registration: true, shared_registration: true, payload_passthru_mode: true } } } }; this._cache = { sessionId: null, reqId: 0, server_wamp_features: { roles: {} }, isSayingGoodbye: false, opStatus: { code: 0, error: null, reqId: 0 }, timer: null, reconnectingAttempts: 0, connectPromise: null, closePromise: null }; this._ws = null; this._wsQueue = []; this._requests = {}; this._calls = {}; this._subscriptionsById = /* @__PURE__ */ new Map(); this._subscriptionsByKey = /* @__PURE__ */ new Map(); this._rpcRegs = {}; this._rpcNames = /* @__PURE__ */ new Set(); this._options = { debug: false, logger: null, autoReconnect: true, reconnectInterval: 2 * 1e3, maxRetries: 25, realm: null, helloCustomDetails: null, uriValidation: "strict", authid: null, authmethods: [], authextra: {}, authPlugins: {}, authMode: "manual", onChallenge: null, onClose: null, onError: null, onReconnect: null, onReconnectSuccess: null, ws: null, additionalHeaders: null, wsRequestOptions: null, serializer: jsonSerializer, payloadSerializers: { json: jsonSerializer } }; if (this._isPlainObject(options)) { this._options = { ...this._options, ...options }; } else if (this._isPlainObject(url)) { this._options = { ...this._options, ...url }; } } /* Internal utils methods */ /** Internal logger */ _log(...args) { if (!this._options.debug) { return; } if (this._options.logger) { return this._options.logger(args); } return console.log("[wampy]", args); } /** Get the new unique request id */ _getReqId() { return ++this._cache.reqId; } /** Check if input is an object literal */ _isPlainObject(input) { const constructor = input?.constructor; const prototype = constructor?.prototype; return Object.prototype.toString.call(input) === "[object Object]" && typeof constructor === "function" && Object.prototype.toString.call(prototype) === "[object Object]" && Object.hasOwnProperty.call(prototype, "isPrototypeOf"); } /** Set websocket protocol based on options */ _setWsProtocols() { this._protocols = ["wamp.2." + this._options.serializer.protocol]; } /** Fill instance operation status */ _fillOpStatusByError(err) { this._cache.opStatus = { code: err.code, error: err, reqId: 0 }; } /** Prerequisite checks for any wampy api call */ _preReqChecks(topicType, role) { if (this._cache.sessionId && !this._cache.server_wamp_features.roles[role]) { const errorsByRole = { dealer: new NoDealerError(), broker: new NoBrokerError() }; this._fillOpStatusByError(errorsByRole[role]); return false; } if (topicType && !this._validateURI(topicType.topic, topicType.patternBased, topicType.allowWAMP)) { this._fillOpStatusByError(new UriError()); return false; } return true; } /** Check for specified feature in a role of connected WAMP Router */ _checkRouterFeature(role, feature) { if (!this._cache.server_wamp_features.roles[role].features[feature]) { this._fillOpStatusByError(new FeatureNotSupportedError(role, feature)); return false; } return true; } /** Check for PPT mode options correctness */ _checkPPTOptions(role, options) { if (!this._checkRouterFeature(role, "payload_passthru_mode")) { this._fillOpStatusByError(new PPTNotSupportedError()); return false; } if (options.ppt_scheme.search(/^(wamp$|mqtt$|x_)/) < 0) { this._fillOpStatusByError(new PPTInvalidSchemeError()); return false; } if (options.ppt_scheme === "wamp" && !E2EE_SERIALIZERS.includes(options.ppt_serializer)) { this._fillOpStatusByError(new PPTSerializerInvalidError()); return false; } return true; } /** Validate uri */ _validateURI(uri, isPatternBased, isWampAllowed) { const isStrictValidation = this._options.uriValidation === "strict"; const isLooseValidation = this._options.uriValidation === "loose"; const isValidationTypeUnknown = !isStrictValidation && !isLooseValidation; if (isValidationTypeUnknown || uri.startsWith("wamp.") && !isWampAllowed) { return false; } let reBase, rePattern; if (isStrictValidation) { reBase = /^(\w+\.)*(\w+)$/; rePattern = /^(\w+\.{1,2})*(\w+)$/; } else if (isLooseValidation) { reBase = /^([^\s#.]+\.)*([^\s#.]+)$/; rePattern = /^([^\s#.]+\.{1,2})*([^\s#.]+)$/; } return (isPatternBased ? rePattern : reBase).test(uri); } /** Prepares PPT/E2EE payload for adding to WAMP message */ _packPPTPayload(payload, options) { const payloadObj = payload; const isArgsListInvalid = payloadObj?.argsList && !Array.isArray(payloadObj.argsList); const isArgsDictInvalid = payloadObj?.argsDict && !this._isPlainObject(payloadObj.argsDict); if (isArgsListInvalid || isArgsDictInvalid) { const invalidParameter = isArgsListInvalid ? payloadObj.argsList : payloadObj.argsDict; this._fillOpStatusByError(new InvalidParamError(String(invalidParameter))); return { err: true, payloadItems: [] }; } const isPayloadAnObject = this._isPlainObject(payload); const { argsList, argsDict } = payloadObj ?? {}; let args, kwargs; if (isPayloadAnObject && !argsList && !argsDict) { kwargs = payload; } else if (isPayloadAnObject) { args = argsList; kwargs = argsDict; } else if (Array.isArray(payload)) { args = payload; } else { args = [payload]; } const payloadItems = []; if (!options.ppt_scheme) { if (args) { payloadItems.push(args); } if (kwargs) { if (!args) { payloadItems.push([]); } payloadItems.push(kwargs); } return { err: false, payloadItems }; } const pptPayload = { args, kwargs }; let binPayload = pptPayload; if (options.ppt_serializer && options.ppt_serializer !== "native") { const pptSerializer = this._options.payloadSerializers[options.ppt_serializer]; if (!pptSerializer) { this._fillOpStatusByError(new PPTSerializerInvalidError()); return { err: true, payloadItems }; } try { binPayload = pptSerializer.encode(pptPayload); } catch { this._fillOpStatusByError(new PPTSerializationError()); return { err: true, payloadItems }; } } payloadItems.push([binPayload]); return { err: false, payloadItems }; } /** Unpack PPT/E2EE payload to common */ _unpackPPTPayload(role, pptPayload, options) { let decodedPayload; if (!this._checkPPTOptions(role, options)) { return { err: this._cache.opStatus.error || false }; } if (options.ppt_serializer && options.ppt_serializer !== "native") { const pptSerializer = this._options.payloadSerializers[options.ppt_serializer]; if (!pptSerializer) { return { err: new PPTSerializerInvalidError() }; } try { decodedPayload = pptSerializer.decode(pptPayload); } catch { return { err: new PPTSerializationError() }; } } else { decodedPayload = pptPayload; } return { err: false, args: decodedPayload.args, kwargs: decodedPayload.kwargs }; } /** Encode WAMP message */ _encode(msg) { try { return this._options.serializer.encode(msg); } catch { this._hardClose("wamp.error.protocol_violation", "Can not encode message", true); } } /** Decode WAMP message */ _decode(msg) { try { return this._options.serializer.decode(msg); } catch { this._hardClose("wamp.error.protocol_violation", "Can not decode received message"); return []; } } /** Hard close of connection due to protocol violations */ _hardClose(errorUri, details, noSend = false) { this._log(details); this._wsQueue = []; if (!noSend) { this._send([WAMP_MSG_SPEC.ABORT, { message: details }, errorUri]); } const protocolViolationError = new ProtocolViolationError(errorUri, details); if (this._cache.connectPromise) { this._cache.connectPromise.onError(protocolViolationError); this._cache.connectPromise = null; } if (this._options.onError) { this._options.onError(protocolViolationError); } this._ws.close(); } /** Send encoded message to server */ _send(msg) { if (msg) { this._wsQueue.push(this._encode(msg)); } if (this._ws && this._ws.readyState === 1 && this._cache.sessionId) { while (this._wsQueue.length > 0) { this._ws.send(this._wsQueue.shift()); } } } /** Reject (fail) all ongoing promises on connection closing */ async _reject_ongoing_promises(error) { const promises = []; for (const call of Object.values(this._calls)) { if (call.onError) { promises.push(call.onError(error)); } } for (const req of Object.values(this._requests)) { if (req.callbacks?.onError) { promises.push(req.callbacks.onError(error)); } } await Promise.allSettled(promises); this._requests = {}; this._calls = {}; } /** Reset internal state and cache */ _resetState() { this._wsQueue = []; this._subscriptionsById.clear(); this._subscriptionsByKey.clear(); this._requests = {}; this._calls = {}; this._rpcRegs = {}; this._rpcNames = /* @__PURE__ */ new Set(); this._cache = { reqId: 0, reconnectingAttempts: 0, opStatus: SUCCESS, closePromise: null, connectPromise: null }; } /** Initialize internal websocket callbacks */ _initWsCallbacks() { this._ws.onopen = () => this._wsOnOpen(); this._ws.onclose = async (event) => this._wsOnClose(event); this._ws.onmessage = (event) => this._wsOnMessage(event); this._ws.onerror = async (error) => this._wsOnError(error); } /** Internal websocket on open callback */ _wsOnOpen() { const { helloCustomDetails, authmethods, authid, authextra, serializer, onError, realm } = this._options; const serverProtocol = this._ws.protocol?.split(".")?.[2]; const hasServerChosenOurPreferredProtocol = serverProtocol === serializer.protocol; this._log(`Websocket connected. Server has chosen protocol: "${serverProtocol}"`); if (!hasServerChosenOurPreferredProtocol) { if (serverProtocol === "json") { this._options.serializer = new JsonSerializer(); } else { const noSerializerAvailableError = new NoSerializerAvailableError(); this._fillOpStatusByError(noSerializerAvailableError); if (this._cache.connectPromise) { this._cache.connectPromise.onError(noSerializerAvailableError); this._cache.connectPromise = null; } if (onError) { onError(noSerializerAvailableError); } } } if (serializer.isBinary) { this._ws.binaryType = "arraybuffer"; } const messageOptions = { ...helloCustomDetails, ...this._wamp_features, ...authid ? { authid, authmethods, authextra } : {} }; const encodedMessage = this._encode([WAMP_MSG_SPEC.HELLO, realm, messageOptions]); if (encodedMessage) { this._ws.send(encodedMessage); } } /** Internal websocket on close callback */ async _wsOnClose(event) { this._log("websocket disconnected. Info: ", event); await this._reject_ongoing_promises(new WebsocketError("Connection closed")); if ((this._cache.sessionId || this._cache.reconnectingAttempts) && this._options.autoReconnect && (this._options.maxRetries === 0 || this._cache.reconnectingAttempts < this._options.maxRetries) && !this._cache.isSayingGoodbye) { this._cache.sessionId = null; this._cache.timer = setTimeout(() => { this._wsReconnect(); }, this._options.reconnectInterval); } else { if (this._options.onClose) { this._options.onClose(); } if (this._cache.closePromise) { this._cache.closePromise.onSuccess(void 0); this._cache.closePromise = null; } this._resetState(); this._ws = null; } } /** Internal websocket on event callback */ async _wsOnMessage(event) { const data = this._decode(event.data); this._log("websocket message received: ", data); const messageType = data[0]; const messageHandlers = { [WAMP_MSG_SPEC.WELCOME]: () => this._onWelcomeMessage(data), [WAMP_MSG_SPEC.ABORT]: () => this._onAbortMessage(data), [WAMP_MSG_SPEC.CHALLENGE]: () => this._onChallengeMessage(data), [WAMP_MSG_SPEC.GOODBYE]: () => this._onGoodbyeMessage(), [WAMP_MSG_SPEC.ERROR]: () => this._onErrorMessage(data), [WAMP_MSG_SPEC.SUBSCRIBED]: () => this._onSubscribedMessage(data), [WAMP_MSG_SPEC.UNSUBSCRIBED]: () => this._onUnsubscribedMessage(data), [WAMP_MSG_SPEC.PUBLISHED]: () => this._onPublishedMessage(data), [WAMP_MSG_SPEC.EVENT]: () => this._onEventMessage(data), [WAMP_MSG_SPEC.RESULT]: () => this._onResultMessage(data), // [WAMP_MSG_SPEC.REGISTER]: () => {}, [WAMP_MSG_SPEC.REGISTERED]: () => this._onRegisteredMessage(data), // [WAMP_MSG_SPEC.UNREGISTER]: () => {}, [WAMP_MSG_SPEC.UNREGISTERED]: () => this._onUnregisteredMessage(data), [WAMP_MSG_SPEC.INVOCATION]: () => this._onInvocationMessage(data) // [WAMP_MSG_SPEC.INTERRUPT]: () => {}, // [WAMP_MSG_SPEC.YIELD]: () => {}, }; const handler5 = messageHandlers[messageType]; const errorURI = "wamp.error.protocol_violation"; if (!handler5) { return this._hardClose(errorURI, `Received non-compliant WAMP message: "${messageType}"`); } const needNoSession = [WAMP_MSG_SPEC.WELCOME, WAMP_MSG_SPEC.CHALLENGE].includes(messageType); const needValidSession = !needNoSession && messageType !== WAMP_MSG_SPEC.ABORT; if (needNoSession && this._cache.sessionId) { return this._hardClose(errorURI, `Received message "${messageType}" after session was established`); } if (needValidSession && !this._cache.sessionId) { return this._hardClose(errorURI, `Received message "${messageType}" before session was established`); } if (this._isRequestIdValid(data)) { await handler5(); } } /** Validates the requestId for message types that need this kind of validation */ _isRequestIdValid([messageType, requestId]) { const isRequestIdValidationNeeded = [ WAMP_MSG_SPEC.SUBSCRIBED, WAMP_MSG_SPEC.UNSUBSCRIBED, WAMP_MSG_SPEC.PUBLISHED, WAMP_MSG_SPEC.RESULT, WAMP_MSG_SPEC.REGISTERED, WAMP_MSG_SPEC.UNREGISTERED ].includes(messageType); if (!isRequestIdValidationNeeded) { return true; } if (messageType === WAMP_MSG_SPEC.RESULT && this._calls[requestId]) { return true; } if (this._requests[requestId]) { return true; } return false; } /** * Handles websocket welcome message event * WAMP SPEC: [WELCOME, Session|id, Details|dict] */ async _onWelcomeMessage([, sessionId, details]) { this._cache.sessionId = sessionId; this._cache.server_wamp_features = details; if (this._cache.reconnectingAttempts) { this._cache.reconnectingAttempts = 0; if (this._options.onReconnectSuccess) { await this._options.onReconnectSuccess(details); } await Promise.allSettled([this._renewSubscriptions(), this._renewRegistrations()]); } else { this._cache.connectPromise.onSuccess(details); this._cache.connectPromise = null; } this._send(); } /** * Handles websocket abort message event * WAMP SPEC: [ABORT, Details|dict, Error|uri] */ async _onAbortMessage([, details, error]) { const err = new AbortError({ error, details }); if (this._cache.connectPromise) { this._cache.connectPromise.onError(err); this._cache.connectPromise = null; } if (this._options.onError) { await this._options.onError(err); } this._ws.close(); } /** * Handles websocket challenge message event * WAMP SPEC: [CHALLENGE, AuthMethod|string, Extra|dict] */ async _onChallengeMessage([, authMethod, extra]) { let promise; const { authid, authMode, onChallenge, onError, authPlugins } = this._options; if (authid && authMode === "manual" && typeof onChallenge === "function") { promise = new Promise((resolve) => { resolve(onChallenge(authMethod, extra)); }); } else if (authid && authMode === "auto" && typeof authPlugins[authMethod] === "function") { promise = new Promise((resolve) => { resolve(authPlugins[authMethod](authMethod, extra)); }); } else { const noCRACallbackOrIdError = new NoCRACallbackOrIdError(); this._fillOpStatusByError(noCRACallbackOrIdError); this._ws.send(this._encode([ WAMP_MSG_SPEC.ABORT, { message: noCRACallbackOrIdError.message }, "wamp.error.cannot_authenticate" ])); if (onError) { await onError(noCRACallbackOrIdError); } return this._ws.close(); } try { const key = await promise; this._ws.send(this._encode([WAMP_MSG_SPEC.AUTHENTICATE, key, {}])); } catch { const challengeExceptionError = new ChallengeExceptionError(); this._fillOpStatusByError(challengeExceptionError); this._ws.send(this._encode([ WAMP_MSG_SPEC.ABORT, { message: challengeExceptionError.message }, "wamp.error.cannot_authenticate" ])); if (onError) { await onError(challengeExceptionError); } this._ws.close(); } } /** * Handles websocket goodbye message event * WAMP SPEC: [GOODBYE, Details|dict, Reason|uri] */ async _onGoodbyeMessage() { if (!this._cache.isSayingGoodbye) { this._cache.isSayingGoodbye = true; this._send([WAMP_MSG_SPEC.GOODBYE, {}, "wamp.close.goodbye_and_out"]); } this._cache.sessionId = null; this._ws.close(); } /** * Handles websocket error message event * WAMP SPEC: [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, * Error|uri, (Arguments|list, ArgumentsKw|dict)] */ async _onErrorMessage([, requestType, requestId, details, error, argsList, argsDict]) { const errorOptions = { error, details, argsList, argsDict }; const errorsByRequestType = { [WAMP_MSG_SPEC.SUBSCRIBE]: new SubscribeError(errorOptions), [WAMP_MSG_SPEC.UNSUBSCRIBE]: new UnsubscribeError(errorOptions), [WAMP_MSG_SPEC.PUBLISH]: new PublishError(errorOptions), [WAMP_MSG_SPEC.REGISTER]: new RegisterError(errorOptions), [WAMP_MSG_SPEC.UNREGISTER]: new UnregisterError(errorOptions), // [WAMP_MSG_SPEC.INVOCATION]: [WAMP_MSG_SPEC.CALL]: new CallError(errorOptions) }; const currentError = errorsByRequestType[requestType]; if (!currentError) { return this._hardClose("wamp.error.protocol_violation", "Received invalid ERROR message"); } if (requestType === WAMP_MSG_SPEC.CALL) { const call = this._calls[requestId]; if (call?.onError) { await call.onError(currentError); } delete this._calls[requestId]; } else { const req = this._requests[requestId]; if (req?.callbacks?.onError) { await req.callbacks.onError(currentError); } delete this._requests[requestId]; } } /** * Handles websocket subscribed message event * WAMP SPEC: [SUBSCRIBED, SUBSCRIBE.Request|id, Subscription|id] */ async _onSubscribedMessage([, requestId, subscriptionId]) { const { topic, advancedOptions, callbacks } = this._requests[requestId]; const subscription = { id: subscriptionId, topic, advancedOptions, callbacks: [callbacks.onEvent] }; const subscriptionKey = this._getSubscriptionKey(topic, advancedOptions); this._subscriptionsById.set(subscriptionId, subscription); this._subscriptionsByKey.set(subscriptionKey, subscription); if (callbacks.onSuccess) { await callbacks.onSuccess({ topic, requestId, subscriptionId, subscriptionKey }); } delete this._requests[requestId]; } /** * Handles websocket unsubscribed message event * WAMP SPEC: [UNSUBSCRIBED, UNSUBSCRIBE.Request|id] */ async _onUnsubscribedMessage([, requestId]) { const { topic, advancedOptions, callbacks } = this._requests[requestId]; const subscriptionKey = this._getSubscriptionKey(topic, advancedOptions); const subscriptionId = this._subscriptionsByKey.get(subscriptionKey).id; this._subscriptionsByKey.delete(subscriptionKey); this._subscriptionsById.delete(subscriptionId); if (callbacks.onSuccess) { await callbacks.onSuccess({ topic, requestId }); } delete this._requests[requestId]; } /** * Handles websocket published message event * WAMP SPEC: [PUBLISHED, PUBLISH.Request|id, Publication|id] */ async _onPublishedMessage([, requestId, publicationId]) { const { topic, callbacks } = this._requests[requestId]; if (callbacks?.onSuccess) { await callbacks.onSuccess({ topic, requestId, publicationId }); } delete this._requests[requestId]; } /** * Handles websocket event message event * WAMP SPEC: [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, * Details|dict, PUBLISH.Arguments|list, PUBLISH.ArgumentKw|dict] */ async _onEventMessage([, subscriptionId, publicationId, details, argsList, argsDict]) { const subscription = this._subscriptionsById.get(subscriptionId); if (!subscription) { return; } let args = argsList; let kwargs = argsDict; if (details.ppt_scheme) { const pptPayload = argsList[0]; const decodedPayload = this._unpackPPTPayload("broker", pptPayload, details); if (decodedPayload.err) { return this._log(decodedPayload.err.message); } args = decodedPayload.args; kwargs = decodedPayload.kwargs; } const callbackOptions = { details, argsList: args, argsDict: kwargs }; const callbackPromises = subscription.callbacks.map((c) => c(callbackOptions)); await Promise.all(callbackPromises); } /** * Handles websocket result message event * WAMP SPEC: [RESULT, CALL.Request|id, Details|dict, * YIELD.Arguments|list, YIELD.ArgumentsKw|dict] */ async _onResultMessage([, requestId, details, argsList, argsDict]) { let args = argsList; let kwargs = argsDict; if (details.ppt_scheme) { const pptPayload = argsList[0]; const decodedPayload = this._unpackPPTPayload("dealer", pptPayload, details); if (decodedPayload.err) { this._log(decodedPayload.err.message); this._cache.opStatus = decodedPayload.err; await this._calls[requestId].onError(new CallError({ details, error: "wamp.error.invocation_exception", argsList: [decodedPayload.err.message], argsDict: void 0 })); delete this._calls[requestId]; return; } args = decodedPayload.args; kwargs = decodedPayload.kwargs; } const callbackOptions = { details, argsList: args, argsDict: kwargs }; if (details.progress) { await this._calls[requestId].onProgress(callbackOptions); } else { await this._calls[requestId].onSuccess(callbackOptions); delete this._calls[requestId]; } } /** * Handles websocket registered message event * WAMP SPEC: [REGISTERED, REGISTER.Request|id, Registration|id] */ async _onRegisteredMessage([, requestId, registrationId]) { const { topic, callbacks, options } = this._requests[requestId]; this._rpcRegs[registrationId] = { id: registrationId, callbacks: [callbacks.rpc], options }; this._rpcRegs[topic] = this._rpcRegs[registrationId]; this._rpcNames.add(topic); if (callbacks?.onSuccess) { await callbacks.onSuccess({ topic, requestId, registrationId }); } delete this._requests[requestId]; } /** * Handles websocket unregistered message event * WAMP SPEC: [UNREGISTERED, UNREGISTER.Request|id] */ async _onUnregisteredMessage([, requestId]) { const { topic, callbacks } = this._requests[requestId]; delete this._rpcRegs[this._rpcRegs[topic].id]; delete this._rpcRegs[topic]; if (this._rpcNames.has(topic)) { this._rpcNames.delete(topic); } if (callbacks?.onSuccess) { await callbacks.onSuccess({ topic, requestId }); } delete this._requests[requestId]; } /** * Handles websocket invocation message event * WAMP SPEC: [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, * CALL.Arguments|list, CALL.ArgumentsKw|dict] */ async _onInvocationMessage([, requestId, registrationId, details, argsList, argsDict]) { const self = this; const handleInvocationError = ({ error, details: details2, argsList: argsList2, argsDict: argsDict2 }) => { const message = [ WAMP_MSG_SPEC.ERROR, WAMP_MSG_SPEC.INVOCATION, requestId, details2 || {}, error || "wamp.error.invocation_exception" ]; if (Array.isArray(argsList2)) { message.push(argsList2); } if (self._isPlainObject(argsDict2)) { if (!Array.isArray(argsList2)) { message.push([]); } message.push(argsDict2); } self._send(message); }; if (!this._rpcRegs[registrationId]) { this._log(WAMP_ERROR_MSG.NON_EXIST_RPC_INVOCATION); return handleInvocationError({ error: "wamp.error.no_such_procedure" }); } let args = argsList; let kwargs = argsDict; if (details?.ppt_scheme) { const pptPayload = argsList[0]; const decodedPayload = this._unpackPPTPayload("dealer", pptPayload, details); if (decodedPayload.err) { this._log(decodedPayload.err.message); if (decodedPayload.err instanceof PPTNotSupportedError) { return this._hardClose( "wamp.error.protocol_violation", "Received INVOCATION in PPT Mode, while Dealer didn't announce it" ); } return handleInvocationError({ details, error: "wamp.error.invocation_exception", argsList: [decodedPayload.err.message] }); } args = decodedPayload.args; kwargs = decodedPayload.kwargs; } const handleInvocationResult = (result) => { const options = result?.options || {}; const { ppt_scheme, ppt_serializer, ppt_cipher, ppt_keyid } = options; if (ppt_scheme && !this._checkPPTOptions("dealer", options)) { if (this._cache.opStatus.error instanceof PPTNotSupportedError) { return this._hardClose( "wamp.error.protocol_violation", "Trying to send YIELD in PPT Mode, while Dealer didn't announce it" ); } return handleInvocationError({ details: options, error: "wamp.error.invalid_option", argsList: [this._cache.opStatus.error.message] }); } const { err, payloadItems } = result ? this._packPPTPayload(result, options) : {}; if (err) { return handleInvocationError({ details: options, error: "wamp.error.invocation_exception", argsList: [this._cache.opStatus.error.message] }); } const messageOptions = { ...options, ...ppt_scheme ? { ppt_scheme } : {}, ...ppt_serializer ? { ppt_serializer } : {}, ...ppt_cipher ? { ppt_cipher } : {}, ...ppt_keyid ? { ppt_keyid } : {}, ...this._extractCustomOptions(options) }; self._send([WAMP_MSG_SPEC.YIELD, requestId, messageOptions, ...payloadItems || []]); }; try { const result = await this._rpcRegs[registrationId].callbacks[0]({ details, argsList: args, argsDict: kwargs, result_handler: handleInvocationResult, error_handler: handleInvocationError }); handleInvocationResult(result); } catch (e) { handleInvocationError(e); } } /** Internal websocket on error callback */ async _wsOnError(error) { this._log("websocket error"); const websocketError = new WebsocketError(error); await this._reject_ongoing_promises(websocketError); if (this._cache.connectPromise) { this._cache.connectPromise.onError(websocketError); this._cache.connectPromise = null; } if (this._options.onError) { this._options.onError(websocketError); } } /** Reconnect to server in case of websocket error */ _wsReconnect() { this._log("websocket reconnecting..."); if (this._options.onReconnect) { this._options.onReconnect(); } this._cache.reconnectingAttempts++; this._ws = getWebSocket({ url: this._url, protocols: this._protocols, options: this._options }); this._initWsCallbacks(); } /** Resubscribe to topics in case of communication error */ async _renewSubscriptions() { let i; const subs = new Map(this._subscriptionsById); this._subscriptionsById.clear(); this._subscriptionsByKey.clear(); for (const sub of subs.values()) { i = sub.callbacks.length; while (i--) { try { await this.subscribe(sub.topic, sub.callbacks[i], sub.advancedOptions); } catch (err) { this._log(`cannot resubscribe to topic: ${sub.topic}`, err); if (this._options.onError) { this._options.onError(err); } } } } } /** ReRegister RPCs in case of communication error */ async _renewRegistrations() { const rpcs = this._rpcRegs, rn = this._rpcNames; this._rpcRegs = {}; this._rpcNames = /* @__PURE__ */ new Set(); for (const rpcName of rn) { try { await this.register(rpcName, rpcs[rpcName].callbacks[0], rpcs[rpcName].options); } catch (err) { this._log(`cannot renew registration of rpc: ${rpcName}`, err); if (this._options.onError) { this._options.onError(err); } } } } /** * Generate a unique key for combination of topic and options * * This is needed to allow subscriptions to the same topic URI but with different options */ _getSubscriptionKey(topic, options) { return `${topic}${options ? `-${JSON.stringify(options)}` : ""}`; } /************************************************************************* * Wampy public API *************************************************************************/ /** Wampy options getter */ getOptions() { return this._options; } /** Wampy options setter */ setOptions(newOptions) { if (this._isPlainObject(newOptions)) { this._options = { ...this._options, ...newOptions }; return this; } } /** * Get the status of last operation * * Returns an object with 3 fields: code, error, reqId * code: 0 - if operation was successful * code > 0 - if error occurred * error: error instance containing details * reqId: last successfully sent request ID */ getOpStatus() { return this._cache.opStatus; } /** Get the WAMP Session ID */ getSessionId() { return this._cache.sessionId; } /** Connect to server */ async connect(url) { if (url) { this._url = url; } if (!this._options.realm) { const noRealmError = new NoRealmError(); this._fillOpStatusByError(noRealmError); throw noRealmError; } const numberOfAuthOptions = (this._options.authid ? 1 : 0) + (Array.isArray(this._options.authmethods) && this._options.authmethods.length > 0 ? 1 : 0) + (typeof this._options.onChallenge === "function" || Object.keys(this._options.authPlugins).length > 0 ? 1 : 0); if (numberOfAuthOptions > 0 && numberOfAuthOptions < 3) { const noCRACallbackOrIdError = new NoCRACallbackOrIdError(); this._fillOpStatusByError(noCRACallbackOrIdError); throw noCRACallbackOrIdError; } this._setWsProtocols(); this._ws = getWebSocket({ url: this._url, protocols: this._protocols, options: this._options }); if (!this._ws) { const noWsOrUrlError = new NoWsOrUrlError(); this._fillOpStatusByError(noWsOrUrlError); throw noWsOrUrlError; } this._initWsCallbacks(); const defer = getNewPromise();