wampy
Version:
Amazingly fast, feature-rich, lightweight WAMP Javascript client (for browser and node.js)
1,500 lines (1,491 loc) • 84 kB
JavaScript
#!/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();