- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
425 lines
13 KiB
JavaScript
425 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
var buffer = require('buffer');
|
|
var eventemitter3 = require('eventemitter3');
|
|
|
|
// node_modules/esbuild-plugin-polyfill-node/polyfills/buffer.js
|
|
var WebSocketBrowserImpl = class extends eventemitter3.EventEmitter {
|
|
socket;
|
|
/** Instantiate a WebSocket class
|
|
* @constructor
|
|
* @param {String} address - url to a websocket server
|
|
* @param {WebSocketBrowserOptions} options - websocket options
|
|
* @return {WebSocketBrowserImpl} - returns a WebSocket instance
|
|
*/
|
|
constructor(address, options) {
|
|
super();
|
|
this.socket = new window.WebSocket(address, options.protocols);
|
|
this.socket.onopen = () => this.emit("open");
|
|
this.socket.onmessage = (event) => this.emit("message", event.data);
|
|
this.socket.onerror = (error) => this.emit("error", error);
|
|
this.socket.onclose = (event) => {
|
|
this.emit("close", event.code, event.reason);
|
|
};
|
|
}
|
|
/**
|
|
* Sends data through a websocket connection
|
|
* @method
|
|
* @param {(String|Object)} data - data to be sent via websocket
|
|
* @param {Object} optionsOrCallback - ws options
|
|
* @param {Function} callback - a callback called once the data is sent
|
|
* @return {Undefined}
|
|
*/
|
|
send(data, optionsOrCallback, callback) {
|
|
const cb = callback || optionsOrCallback;
|
|
try {
|
|
this.socket.send(data);
|
|
cb();
|
|
} catch (error) {
|
|
cb(error);
|
|
}
|
|
}
|
|
/**
|
|
* Closes an underlying socket
|
|
* @method
|
|
* @param {Number} code - status code explaining why the connection is being closed
|
|
* @param {String} reason - a description why the connection is closing
|
|
* @return {Undefined}
|
|
* @throws {Error}
|
|
*/
|
|
close(code, reason) {
|
|
this.socket.close(code, reason);
|
|
}
|
|
addEventListener(type, listener, options) {
|
|
this.socket.addEventListener(type, listener, options);
|
|
}
|
|
};
|
|
function WebSocket(address, options) {
|
|
return new WebSocketBrowserImpl(address, options);
|
|
}
|
|
|
|
// src/lib/utils.ts
|
|
var DefaultDataPack = class {
|
|
encode(value) {
|
|
return JSON.stringify(value);
|
|
}
|
|
decode(value) {
|
|
return JSON.parse(value);
|
|
}
|
|
};
|
|
|
|
// src/lib/client.ts
|
|
var CommonClient = class extends eventemitter3.EventEmitter {
|
|
address;
|
|
rpc_id;
|
|
queue;
|
|
options;
|
|
autoconnect;
|
|
ready;
|
|
reconnect;
|
|
reconnect_timer_id;
|
|
reconnect_interval;
|
|
max_reconnects;
|
|
rest_options;
|
|
current_reconnects;
|
|
generate_request_id;
|
|
socket;
|
|
webSocketFactory;
|
|
dataPack;
|
|
/**
|
|
* Instantiate a Client class.
|
|
* @constructor
|
|
* @param {webSocketFactory} webSocketFactory - factory method for WebSocket
|
|
* @param {String} address - url to a websocket server
|
|
* @param {Object} options - ws options object with reconnect parameters
|
|
* @param {Function} generate_request_id - custom generation request Id
|
|
* @param {DataPack} dataPack - data pack contains encoder and decoder
|
|
* @return {CommonClient}
|
|
*/
|
|
constructor(webSocketFactory, address = "ws://localhost:8080", {
|
|
autoconnect = true,
|
|
reconnect = true,
|
|
reconnect_interval = 1e3,
|
|
max_reconnects = 5,
|
|
...rest_options
|
|
} = {}, generate_request_id, dataPack) {
|
|
super();
|
|
this.webSocketFactory = webSocketFactory;
|
|
this.queue = {};
|
|
this.rpc_id = 0;
|
|
this.address = address;
|
|
this.autoconnect = autoconnect;
|
|
this.ready = false;
|
|
this.reconnect = reconnect;
|
|
this.reconnect_timer_id = void 0;
|
|
this.reconnect_interval = reconnect_interval;
|
|
this.max_reconnects = max_reconnects;
|
|
this.rest_options = rest_options;
|
|
this.current_reconnects = 0;
|
|
this.generate_request_id = generate_request_id || (() => typeof this.rpc_id === "number" ? ++this.rpc_id : Number(this.rpc_id) + 1);
|
|
if (!dataPack) this.dataPack = new DefaultDataPack();
|
|
else this.dataPack = dataPack;
|
|
if (this.autoconnect)
|
|
this._connect(this.address, {
|
|
autoconnect: this.autoconnect,
|
|
reconnect: this.reconnect,
|
|
reconnect_interval: this.reconnect_interval,
|
|
max_reconnects: this.max_reconnects,
|
|
...this.rest_options
|
|
});
|
|
}
|
|
/**
|
|
* Connects to a defined server if not connected already.
|
|
* @method
|
|
* @return {Undefined}
|
|
*/
|
|
connect() {
|
|
if (this.socket) return;
|
|
this._connect(this.address, {
|
|
autoconnect: this.autoconnect,
|
|
reconnect: this.reconnect,
|
|
reconnect_interval: this.reconnect_interval,
|
|
max_reconnects: this.max_reconnects,
|
|
...this.rest_options
|
|
});
|
|
}
|
|
/**
|
|
* Calls a registered RPC method on server.
|
|
* @method
|
|
* @param {String} method - RPC method name
|
|
* @param {Object|Array} params - optional method parameters
|
|
* @param {Number} timeout - RPC reply timeout value
|
|
* @param {Object} ws_opts - options passed to ws
|
|
* @return {Promise}
|
|
*/
|
|
call(method, params, timeout, ws_opts) {
|
|
if (!ws_opts && "object" === typeof timeout) {
|
|
ws_opts = timeout;
|
|
timeout = null;
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.ready) return reject(new Error("socket not ready"));
|
|
const rpc_id = this.generate_request_id(method, params);
|
|
const message = {
|
|
jsonrpc: "2.0",
|
|
method,
|
|
params: params || void 0,
|
|
id: rpc_id
|
|
};
|
|
this.socket.send(this.dataPack.encode(message), ws_opts, (error) => {
|
|
if (error) return reject(error);
|
|
this.queue[rpc_id] = { promise: [resolve, reject] };
|
|
if (timeout) {
|
|
this.queue[rpc_id].timeout = setTimeout(() => {
|
|
delete this.queue[rpc_id];
|
|
reject(new Error("reply timeout"));
|
|
}, timeout);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Logins with the other side of the connection.
|
|
* @method
|
|
* @param {Object} params - Login credentials object
|
|
* @return {Promise}
|
|
*/
|
|
async login(params) {
|
|
const resp = await this.call("rpc.login", params);
|
|
if (!resp) throw new Error("authentication failed");
|
|
return resp;
|
|
}
|
|
/**
|
|
* Fetches a list of client's methods registered on server.
|
|
* @method
|
|
* @return {Array}
|
|
*/
|
|
async listMethods() {
|
|
return await this.call("__listMethods");
|
|
}
|
|
/**
|
|
* Sends a JSON-RPC 2.0 notification to server.
|
|
* @method
|
|
* @param {String} method - RPC method name
|
|
* @param {Object} params - optional method parameters
|
|
* @return {Promise}
|
|
*/
|
|
notify(method, params) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.ready) return reject(new Error("socket not ready"));
|
|
const message = {
|
|
jsonrpc: "2.0",
|
|
method,
|
|
params
|
|
};
|
|
this.socket.send(this.dataPack.encode(message), (error) => {
|
|
if (error) return reject(error);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Subscribes for a defined event.
|
|
* @method
|
|
* @param {String|Array} event - event name
|
|
* @return {Undefined}
|
|
* @throws {Error}
|
|
*/
|
|
async subscribe(event) {
|
|
if (typeof event === "string") event = [event];
|
|
const result = await this.call("rpc.on", event);
|
|
if (typeof event === "string" && result[event] !== "ok")
|
|
throw new Error(
|
|
"Failed subscribing to an event '" + event + "' with: " + result[event]
|
|
);
|
|
return result;
|
|
}
|
|
/**
|
|
* Unsubscribes from a defined event.
|
|
* @method
|
|
* @param {String|Array} event - event name
|
|
* @return {Undefined}
|
|
* @throws {Error}
|
|
*/
|
|
async unsubscribe(event) {
|
|
if (typeof event === "string") event = [event];
|
|
const result = await this.call("rpc.off", event);
|
|
if (typeof event === "string" && result[event] !== "ok")
|
|
throw new Error("Failed unsubscribing from an event with: " + result);
|
|
return result;
|
|
}
|
|
/**
|
|
* Closes a WebSocket connection gracefully.
|
|
* @method
|
|
* @param {Number} code - socket close code
|
|
* @param {String} data - optional data to be sent before closing
|
|
* @return {Undefined}
|
|
*/
|
|
close(code, data) {
|
|
if (this.socket) this.socket.close(code || 1e3, data);
|
|
}
|
|
/**
|
|
* Enable / disable automatic reconnection.
|
|
* @method
|
|
* @param {Boolean} reconnect - enable / disable reconnection
|
|
* @return {Undefined}
|
|
*/
|
|
setAutoReconnect(reconnect) {
|
|
this.reconnect = reconnect;
|
|
}
|
|
/**
|
|
* Set the interval between reconnection attempts.
|
|
* @method
|
|
* @param {Number} interval - reconnection interval in milliseconds
|
|
* @return {Undefined}
|
|
*/
|
|
setReconnectInterval(interval) {
|
|
this.reconnect_interval = interval;
|
|
}
|
|
/**
|
|
* Set the maximum number of reconnection attempts.
|
|
* @method
|
|
* @param {Number} max_reconnects - maximum reconnection attempts
|
|
* @return {Undefined}
|
|
*/
|
|
setMaxReconnects(max_reconnects) {
|
|
this.max_reconnects = max_reconnects;
|
|
}
|
|
/**
|
|
* Get the current number of reconnection attempts made.
|
|
* @method
|
|
* @return {Number} current reconnection attempts
|
|
*/
|
|
getCurrentReconnects() {
|
|
return this.current_reconnects;
|
|
}
|
|
/**
|
|
* Get the maximum number of reconnection attempts.
|
|
* @method
|
|
* @return {Number} maximum reconnection attempts
|
|
*/
|
|
getMaxReconnects() {
|
|
return this.max_reconnects;
|
|
}
|
|
/**
|
|
* Check if the client is currently attempting to reconnect.
|
|
* @method
|
|
* @return {Boolean} true if reconnection is in progress
|
|
*/
|
|
isReconnecting() {
|
|
return this.reconnect_timer_id !== void 0;
|
|
}
|
|
/**
|
|
* Check if the client will attempt to reconnect on the next close event.
|
|
* @method
|
|
* @return {Boolean} true if reconnection will be attempted
|
|
*/
|
|
willReconnect() {
|
|
return this.reconnect && (this.max_reconnects === 0 || this.current_reconnects < this.max_reconnects);
|
|
}
|
|
/**
|
|
* Connection/Message handler.
|
|
* @method
|
|
* @private
|
|
* @param {String} address - WebSocket API address
|
|
* @param {Object} options - ws options object
|
|
* @return {Undefined}
|
|
*/
|
|
_connect(address, options) {
|
|
clearTimeout(this.reconnect_timer_id);
|
|
this.socket = this.webSocketFactory(address, options);
|
|
this.socket.addEventListener("open", () => {
|
|
this.ready = true;
|
|
this.emit("open");
|
|
this.current_reconnects = 0;
|
|
});
|
|
this.socket.addEventListener("message", ({ data: message }) => {
|
|
if (message instanceof ArrayBuffer)
|
|
message = buffer.Buffer.from(message).toString();
|
|
try {
|
|
message = this.dataPack.decode(message);
|
|
} catch (_error) {
|
|
return;
|
|
}
|
|
if (message.notification && this.listeners(message.notification).length) {
|
|
if (!Object.keys(message.params).length)
|
|
return this.emit(message.notification);
|
|
const args = [message.notification];
|
|
if (message.params.constructor === Object) args.push(message.params);
|
|
else
|
|
for (let i = 0; i < message.params.length; i++)
|
|
args.push(message.params[i]);
|
|
return Promise.resolve().then(() => {
|
|
this.emit.apply(this, args);
|
|
});
|
|
}
|
|
if (!this.queue[message.id]) {
|
|
if (message.method) {
|
|
return Promise.resolve().then(() => {
|
|
this.emit(message.method, message?.params);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if ("error" in message === "result" in message)
|
|
this.queue[message.id].promise[1](
|
|
new Error(
|
|
'Server response malformed. Response must include either "result" or "error", but not both.'
|
|
)
|
|
);
|
|
if (this.queue[message.id].timeout)
|
|
clearTimeout(this.queue[message.id].timeout);
|
|
if (message.error) this.queue[message.id].promise[1](message.error);
|
|
else this.queue[message.id].promise[0](message.result);
|
|
delete this.queue[message.id];
|
|
});
|
|
this.socket.addEventListener("error", (error) => this.emit("error", error));
|
|
this.socket.addEventListener("close", ({ code, reason }) => {
|
|
if (this.ready)
|
|
setTimeout(() => this.emit("close", code, reason), 0);
|
|
this.ready = false;
|
|
this.socket = void 0;
|
|
if (code === 1e3) return;
|
|
this.current_reconnects++;
|
|
if (this.reconnect && (this.max_reconnects > this.current_reconnects || this.max_reconnects === 0))
|
|
this.reconnect_timer_id = setTimeout(
|
|
() => this._connect(address, options),
|
|
this.reconnect_interval
|
|
);
|
|
else if (this.reconnect && this.max_reconnects > 0 && this.current_reconnects >= this.max_reconnects) {
|
|
setTimeout(() => this.emit("max_reconnects_reached", code, reason), 1);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/index.browser.ts
|
|
var Client = class extends CommonClient {
|
|
constructor(address = "ws://localhost:8080", {
|
|
autoconnect = true,
|
|
reconnect = true,
|
|
reconnect_interval = 1e3,
|
|
max_reconnects = 5,
|
|
...rest_options
|
|
} = {}, generate_request_id) {
|
|
super(
|
|
WebSocket,
|
|
address,
|
|
{
|
|
autoconnect,
|
|
reconnect,
|
|
reconnect_interval,
|
|
max_reconnects,
|
|
...rest_options
|
|
},
|
|
generate_request_id
|
|
);
|
|
}
|
|
};
|
|
|
|
exports.Client = Client;
|
|
exports.CommonClient = CommonClient;
|
|
exports.DefaultDataPack = DefaultDataPack;
|
|
exports.WebSocket = WebSocket;
|
|
//# sourceMappingURL=index.browser.cjs.map
|
|
//# sourceMappingURL=index.browser.cjs.map
|