mirror of
https://github.com/EvernodeXRPL/hpcore.git
synced 2026-04-29 15:37:59 +00:00
* Unified js client lib for browser and nodejs. * Client lib multiple connections support. * Implemented server challenge response. * Contract guid and version validation. * Server key validation. * User json message encoding improvements.
729 lines
26 KiB
JavaScript
729 lines
26 KiB
JavaScript
(() => {
|
|
|
|
// Whether we are in Browser or NodeJs.
|
|
const isBrowser = !(typeof window === 'undefined');
|
|
|
|
// In browser, avoid duplicate initializations.
|
|
if (isBrowser && window.HotPocket)
|
|
return;
|
|
|
|
const supportedHpVersion = "0.0";
|
|
const serverChallengeSize = 16;
|
|
const connectionCheckIntervalMs = 1000;
|
|
const recentActivityThresholdMs = 3000;
|
|
|
|
// External dependency references.
|
|
let WebSocket = null;
|
|
let sodium = null;
|
|
let bson = null;
|
|
|
|
/*--- Included in public interface. ---*/
|
|
const protocols = {
|
|
json: "json",
|
|
bson: "bson" // (Requires nodejs or browserified hp client library on Browser)
|
|
}
|
|
Object.freeze(protocols);
|
|
|
|
/*--- Included in public interface. ---*/
|
|
const events = {
|
|
disconnect: "disconnect",
|
|
contractOutput: "contractOutput",
|
|
contractReadResponse: "contractReadResponse",
|
|
connectionChange: "connectionChange"
|
|
}
|
|
Object.freeze(events);
|
|
|
|
/*--- Included in public interface. ---*/
|
|
const generateKeys = async (privateKeyHex = null) => {
|
|
|
|
await initSodium();
|
|
|
|
if (!privateKeyHex) {
|
|
const keys = sodium.crypto_sign_keypair();
|
|
return {
|
|
privateKey: keys.privateKey,
|
|
publicKey: keys.publicKey
|
|
}
|
|
}
|
|
else {
|
|
const binPrivateKey = hexToUint8Array(privateKeyHex);
|
|
return {
|
|
privateKey: Uint8Array.from(binPrivateKey),
|
|
publicKey: Uint8Array.from(binPrivateKey.slice(32))
|
|
}
|
|
}
|
|
}
|
|
|
|
/*--- Included in public interface. ---*/
|
|
const createClient = async (servers, clientKeys, options) => {
|
|
|
|
const defaultOptions = {
|
|
contractId: null,
|
|
contractVersion: null,
|
|
validServerKeys: null,
|
|
protocol: protocols.json,
|
|
requiredConnectionCount: 1,
|
|
connectionTimeoutMs: 5000
|
|
};
|
|
const opt = options ? { ...defaultOptions, ...options } : defaultOptions;
|
|
|
|
if (!clientKeys)
|
|
throw "clientKeys not specified.";
|
|
if (opt.contractId == "")
|
|
throw "contractId not specified. Specify null to bypass contract id validation.";
|
|
if (opt.contractVersion == "")
|
|
throw "contractVersion not specified. Specify null to bypass contract version validation.";
|
|
if (!opt.protocol || (opt.protocol != protocols.json && opt.protocol != protocols.bson))
|
|
throw "Valid protocol not specified.";
|
|
if (!opt.requiredConnectionCount || opt.requiredConnectionCount == 0)
|
|
throw "requiredConnectionCount must be greater than 0.";
|
|
if (!opt.connectionTimeoutMs || opt.connectionTimeoutMs == 0)
|
|
throw "Connection timeout must be greater than 0.";
|
|
|
|
await initSodium();
|
|
initWebSocket();
|
|
if (opt.protocol == protocols.bson)
|
|
initBson();
|
|
|
|
// Load servers and serverKeys to object keys to avoid duplicates.
|
|
|
|
const serversLookup = {};
|
|
servers && servers.forEach(s => {
|
|
const url = s.trim();
|
|
if (url.length > 0)
|
|
serversLookup[url] = true
|
|
});
|
|
if (Object.keys(serversLookup).length == 0)
|
|
throw "servers not specified.";
|
|
if (opt.requiredConnectionCount > Object.keys(serversLookup).length)
|
|
throw "requiredConnectionCount is higher than no. of servers.";
|
|
|
|
let serverKeysLookup = null;
|
|
if (opt.validServerKeys) {
|
|
serverKeysLookup = {};
|
|
opt.validServerKeys.forEach(k => {
|
|
const key = k.trim();
|
|
if (key.length > 0)
|
|
serverKeysLookup[key] = true
|
|
});
|
|
}
|
|
|
|
if (serverKeysLookup && Object.keys(serverKeysLookup).length == 0)
|
|
throw "serverKeys must contain at least one key. Specify null to bypass key validation.";
|
|
|
|
return new HotPocketClient(opt.contractId, opt.contractVersion, clientKeys, serversLookup, serverKeysLookup, opt.protocol, opt.requiredConnectionCount, opt.connectionTimeoutMs);
|
|
}
|
|
|
|
function HotPocketClient(contractId, contractVersion, clientKeys, serversLookup, serverKeysLookup, protocol, requiredConnectionCount, connectionTimeoutMs) {
|
|
|
|
let emitter = new EventEmitter();
|
|
|
|
const nodes = Object.keys(serversLookup).map(s => {
|
|
return {
|
|
server: s, // Server address.
|
|
connection: null, // Hot Pocket connection (if any).
|
|
lastActivity: 0 // Last connection activity timestamp.
|
|
}
|
|
});
|
|
|
|
let status = 0; //0:none, 1:connected, 2:closed
|
|
|
|
// This will get fired whenever the required connection count gets fullfilled.
|
|
let initialConnectSuccess = null;
|
|
|
|
// Tracks when was the earliest time that we were missing some required connections.
|
|
// 0 indicates we are no missing any connections.
|
|
let connectionsMissingFrom = new Date().getTime();
|
|
|
|
// Checks for missing connections and attempts to establish them.
|
|
const reviewConnections = () => {
|
|
|
|
if (status == 2)
|
|
return;
|
|
|
|
// Check for connection changes periodically.
|
|
setTimeout(() => {
|
|
reviewConnections();
|
|
}, connectionCheckIntervalMs);
|
|
|
|
// Check whether we have fullfilled all required connections.
|
|
if (nodes.filter(n => n.connection && n.connection.isConnected()).length == requiredConnectionCount) {
|
|
connectionsMissingFrom = 0;
|
|
initialConnectSuccess && initialConnectSuccess(true);
|
|
initialConnectSuccess = null;
|
|
status = 1;
|
|
return;
|
|
}
|
|
|
|
if (connectionsMissingFrom == 0) {
|
|
// Reaching here means we moved from connections-fullfilled state to missing-connections state.
|
|
connectionsMissingFrom = new Date().getTime();
|
|
}
|
|
else if ((new Date().getTime() - connectionsMissingFrom) > connectionTimeoutMs) {
|
|
|
|
// This means we were not able to maintain required connection count for the entire timeout period.
|
|
|
|
console.log("Missing-connections timeout reached.");
|
|
|
|
// Close and cleanup all connections if we hit the timeout.
|
|
this.close().then(() => {
|
|
if (initialConnectSuccess) {
|
|
initialConnectSuccess(false);
|
|
initialConnectSuccess = null;
|
|
}
|
|
else {
|
|
emitter && emitter.emit(events.disconnect);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Reaching here means we should attempt to establish more connections if we have available slots.
|
|
let currentConnectionCount = nodes.filter(n => n.connection).length;
|
|
if (currentConnectionCount == requiredConnectionCount)
|
|
return;
|
|
|
|
// Find out available slots.
|
|
// Skip nodes that are already connected or is currently establishing connection.
|
|
// Skip nodes that have recently shown some connection activity.
|
|
// Give priority to nodes that have not shown any activity recently.
|
|
const freeNodes = nodes.filter(n => !n.connection && (new Date().getTime() - n.lastActivity) > recentActivityThresholdMs);
|
|
freeNodes.sort((a, b) => a.lastActivity - b.lastActivity); // Oldest activity comes first.
|
|
|
|
while (currentConnectionCount < requiredConnectionCount && freeNodes.length > 0) {
|
|
|
|
// Get the next available node.
|
|
const n = freeNodes.shift();
|
|
n.connection = new HotPocketConnection(contractId, contractVersion, clientKeys, n.server, serverKeysLookup, protocol, connectionTimeoutMs, emitter);
|
|
n.lastActivity = new Date().getTime();
|
|
|
|
n.connection.connect().then(success => {
|
|
if (!success)
|
|
n.connection = null;
|
|
else
|
|
emitter && emitter.emit(events.connectionChange, n.server, "add");
|
|
});
|
|
|
|
n.connection.onClose = () => {
|
|
n.connection = null;
|
|
emitter && emitter.emit(events.connectionChange, n.server, "remove");
|
|
};
|
|
|
|
currentConnectionCount++;
|
|
}
|
|
}
|
|
|
|
this.connect = () => {
|
|
|
|
if (status > 0)
|
|
return;
|
|
|
|
reviewConnections();
|
|
return new Promise(resolve => {
|
|
initialConnectSuccess = resolve;
|
|
})
|
|
}
|
|
|
|
this.close = async () => {
|
|
|
|
if (status == 2)
|
|
return;
|
|
|
|
status = 2;
|
|
emitter.clear(events.connectionChange);
|
|
emitter.clear(events.contractOutput);
|
|
emitter.clear(events.contractReadResponse);
|
|
|
|
// Close all nodes connections.
|
|
await Promise.all(nodes.filter(n => n.connection).map(n => n.connection.close()));
|
|
nodes.forEach(n => n.connection = null);
|
|
}
|
|
|
|
this.on = (event, listener) => {
|
|
emitter.on(event, listener);
|
|
}
|
|
|
|
this.sendContractInput = async (input, nonce = null, maxLclOffset = null) => {
|
|
if (status == 2)
|
|
return;
|
|
|
|
await Promise.all(
|
|
nodes.filter(n => n.connection && n.connection.isConnected())
|
|
.map(n => n.connection.sendContractInput(input, nonce, maxLclOffset)));
|
|
}
|
|
|
|
this.sendContractReadRequest = (request) => {
|
|
if (status == 2)
|
|
return;
|
|
|
|
nodes.filter(n => n.connection && n.connection.isConnected()).forEach(n => {
|
|
n.connection.sendContractReadRequest(request);
|
|
});
|
|
}
|
|
}
|
|
|
|
function HotPocketConnection(contractId, contractVersion, clientKeys, server, serverKeysLookup, protocol, connectionTimeoutMs, emitter) {
|
|
|
|
// Create message helper with JSON protocol initially.
|
|
// After challenge handshake, we will change this to use the protocol specified by user.
|
|
const msgHelper = new MessageHelper(clientKeys, protocols.json);
|
|
|
|
let connectionStatus = 0; // 0:none, 1:server challenge sent, 2:handshake complete.
|
|
let serverChallenge = null; // The hex challenge we have issued to the server.
|
|
let reportedContractId = null;
|
|
let reportedContractVersion = null;
|
|
|
|
let ws = null;
|
|
let handshakeTimer = null; // Timer to track connection handshake timeout.
|
|
let handshakeResolver = null;
|
|
let closeResolver = null;
|
|
let statResponseResolvers = [];
|
|
let contractInputResolvers = {};
|
|
|
|
const handshakeMessageHandler = (m) => {
|
|
|
|
if (connectionStatus == 0 && m.type == "user_challenge" && m.hp_version && m.contract_id) {
|
|
|
|
if (m.hp_version != supportedHpVersion) {
|
|
console.log(`Incompatible Hot Pocket server version. Expected:${supportedHpVersion} Got:${m.hp_version}`);
|
|
return false;
|
|
}
|
|
else if (!m.contract_id) {
|
|
console.log("Server did not specify contract id.");
|
|
return false;
|
|
}
|
|
else if (contractId && m.contract_id != contractId) {
|
|
console.log(`Contract id mismatch. Expected:${contractId} Got:${m.contract_id}`);
|
|
return false;
|
|
}
|
|
else if (!m.contract_version) {
|
|
console.log("Server did not specify contract version.");
|
|
return false;
|
|
}
|
|
else if (contractVersion && m.contract_version != contractVersion) {
|
|
console.log(`Contract version mismatch. Expected:${contractVersion} Got:${m.contract_version}`);
|
|
return false;
|
|
}
|
|
|
|
reportedContractId = m.contract_id;
|
|
reportedContractVersion = m.contract_version;
|
|
|
|
// Generate the challenge we are sending to server.
|
|
serverChallenge = uint8ArrayToHex(sodium.randombytes_buf(serverChallengeSize));
|
|
|
|
// Sign the challenge and send back the response
|
|
const response = msgHelper.createUserChallengeResponse(m.challenge, serverChallenge, protocol);
|
|
ws.send(msgHelper.serializeObject(response));
|
|
|
|
connectionStatus = 1;
|
|
return true;
|
|
}
|
|
else if (connectionStatus == 1 && serverChallenge && m.type == "server_challenge_response" && m.sig && m.pubkey) {
|
|
|
|
// If server keys has been specified, validate whether this server's pubkey is among the valid list.
|
|
if (serverKeysLookup && !serverKeysLookup[m.pubkey]) {
|
|
console.log(`${server} key '${m.pubkey}' not among the valid keys.`);
|
|
return false;
|
|
}
|
|
|
|
// Verify server challenge response.
|
|
const stringToVerify = serverChallenge + reportedContractId + reportedContractVersion;
|
|
const serverPubkeyHex = m.pubkey.substring(2); // Skip 'ed' prefix;
|
|
if (!sodium.crypto_sign_verify_detached(hexToUint8Array(m.sig), stringToVerify, hexToUint8Array(serverPubkeyHex))) {
|
|
console.log(`${server} challenge response verification failed.`);
|
|
return false;
|
|
}
|
|
|
|
clearTimeout(handshakeTimer); // Cancel the handshake timeout monitor.
|
|
handshakeTimer = null;
|
|
serverChallenge = null; // Clear the sent challenge as we no longer need it.
|
|
msgHelper.useProtocol(protocol); // Here onwards, use the message protocol specified by user.
|
|
connectionStatus = 2; // Handshake complete.
|
|
|
|
// If we are still connected, report handshaking as successful.
|
|
// (If websocket disconnects, handshakeResolver will be already null)
|
|
handshakeResolver && handshakeResolver(true);
|
|
console.log(`Connected to ${server}`);
|
|
return true;
|
|
}
|
|
|
|
console.log(`${server} invalid message during handshake. Connection status:${connectionStatus}`);
|
|
console.log(m);
|
|
return false;
|
|
}
|
|
|
|
const contractMessageHandler = (m) => {
|
|
|
|
if (m.type == "contract_read_response") {
|
|
emitter && emitter.emit(events.contractReadResponse, msgHelper.deserializeOutput(m.content));
|
|
}
|
|
else if (m.type == "contract_input_status") {
|
|
const sigKey = msgHelper.stringifySignature(m.input_sig);
|
|
const resolver = contractInputResolvers[sigKey];
|
|
if (resolver) {
|
|
if (m.status == "accepted")
|
|
resolver("ok");
|
|
else
|
|
resolver(m.reason);
|
|
delete contractInputResolvers[sigKey];
|
|
}
|
|
}
|
|
else if (m.type == "contract_output") {
|
|
emitter && emitter.emit(events.contractOutput, msgHelper.deserializeOutput(m.content));
|
|
}
|
|
else if (m.type == "stat_response") {
|
|
statResponseResolvers.forEach(resolver => {
|
|
resolver({
|
|
lcl: m.lcl,
|
|
lclSeqNo: m.lcl_seqno
|
|
});
|
|
})
|
|
statResponseResolvers = [];
|
|
}
|
|
else {
|
|
console.log("Received unrecognized contract message: type:" + m.type);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const messageHandler = async (rcvd) => {
|
|
|
|
const data = (connectionStatus < 2 || protocol == protocols.json) ?
|
|
(isBrowser ? await rcvd.data.text() : rcvd.data) :
|
|
(isBrowser ? await rcvd.data.arrayBuffer() : rcvd.data);
|
|
|
|
try {
|
|
m = msgHelper.deserializeMessage(data);
|
|
}
|
|
catch (e) {
|
|
console.log(e);
|
|
console.log("Exception deserializing: ");
|
|
console.log(data || rcvd);
|
|
|
|
// If we get invalid message during handshake, close the socket.
|
|
if (connectionStatus < 2)
|
|
this.close();
|
|
|
|
return;
|
|
}
|
|
|
|
let isValid = false;
|
|
if (connectionStatus < 2)
|
|
isValid = handshakeMessageHandler(m);
|
|
else if (connectionStatus == 2)
|
|
isValid = contractMessageHandler(m);
|
|
|
|
if (!isValid) {
|
|
// If we get invalid message during handshake, close the socket.
|
|
if (connectionStatus < 2)
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
const openHandler = () => {
|
|
ws.addEventListener("message", messageHandler);
|
|
ws.addEventListener("close", closeHandler);
|
|
|
|
handshakeTimer = setTimeout(() => {
|
|
// If handshake does not complete within timeout, close the connection.
|
|
this.close();
|
|
handshakeTimer = null;
|
|
}, connectionTimeoutMs);
|
|
}
|
|
|
|
const closeHandler = () => {
|
|
|
|
if (closeResolver)
|
|
console.log("Closing connection to " + server);
|
|
else
|
|
console.log("Disconnected from " + server);
|
|
|
|
emitter = null;
|
|
|
|
if (handshakeTimer)
|
|
clearTimeout(handshakeTimer);
|
|
|
|
// If there are any ongoing resolvers resolve them with error output.
|
|
|
|
handshakeResolver && handshakeResolver(false);
|
|
handshakeResolver = null;
|
|
|
|
statResponseResolvers.forEach(resolver => resolver(null));
|
|
statResponseResolvers = [];
|
|
|
|
Object.values(contractInputResolvers).forEach(resolver => resolver(null));
|
|
contractInputResolvers = {};
|
|
|
|
this.onClose && this.onClose();
|
|
closeResolver && closeResolver();
|
|
}
|
|
|
|
const errorHandler = (e) => {
|
|
handshakeResolver && handshakeResolver(false);
|
|
}
|
|
|
|
this.isConnected = () => {
|
|
return connectionStatus == 2;
|
|
};
|
|
|
|
this.connect = () => {
|
|
console.log("Connecting to " + server);
|
|
return new Promise(resolve => {
|
|
|
|
ws = isBrowser ? new WebSocket(server) : new WebSocket(server, { rejectUnauthorized: false });
|
|
handshakeResolver = resolve;
|
|
ws.addEventListener("error", errorHandler);
|
|
ws.addEventListener("open", openHandler);
|
|
});
|
|
}
|
|
|
|
this.close = () => {
|
|
if (ws.readyState == WebSocket.OPEN) {
|
|
return new Promise(resolve => {
|
|
closeResolver = resolve;
|
|
ws.close();
|
|
});
|
|
}
|
|
else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
|
|
this.getStatus = () => {
|
|
|
|
if (connectionStatus != 2)
|
|
return Promise.resolve(null);
|
|
|
|
const p = new Promise(resolve => {
|
|
statResponseResolvers.push(resolve);
|
|
});
|
|
|
|
// If this is the only awaiting stat request, then send an actual stat request.
|
|
// Otherwise simply wait for the previously sent request.
|
|
if (statResponseResolvers.length == 1) {
|
|
const msg = msgHelper.createStatusRequest();
|
|
ws.send(msgHelper.serializeObject(msg));
|
|
}
|
|
return p;
|
|
}
|
|
|
|
this.sendContractInput = async (input, nonce = null, maxLclOffset = null) => {
|
|
|
|
if (connectionStatus != 2)
|
|
return null;
|
|
|
|
if (!maxLclOffset)
|
|
maxLclOffset = 10;
|
|
|
|
if (!nonce)
|
|
nonce = (new Date()).getTime().toString();
|
|
else
|
|
nonce = nonce.toString();
|
|
|
|
// Acquire the current lcl and add the specified offset.
|
|
const stat = await this.getStatus();
|
|
if (!stat)
|
|
return new Promise(resolve => resolve("ledger_status_error"));
|
|
const maxLclSeqNo = stat.lclSeqNo + maxLclOffset;
|
|
|
|
const msg = msgHelper.createContractInput(input, nonce, maxLclSeqNo);
|
|
const sigKey = msgHelper.stringifySignature(msg.sig);
|
|
const p = new Promise(resolve => {
|
|
contractInputResolvers[sigKey] = resolve;
|
|
});
|
|
|
|
ws.send(msgHelper.serializeObject(msg));
|
|
return p;
|
|
}
|
|
|
|
this.sendContractReadRequest = (request) => {
|
|
|
|
if (connectionStatus != 2)
|
|
return;
|
|
|
|
const msg = msgHelper.createReadRequest(request);
|
|
ws.send(msgHelper.serializeObject(msg));
|
|
}
|
|
}
|
|
|
|
function MessageHelper(keys, protocol) {
|
|
|
|
this.useProtocol = (p) => {
|
|
protocol = p;
|
|
}
|
|
|
|
this.binaryEncode = (data) => {
|
|
return protocol == protocols.json ?
|
|
uint8ArrayToHex(data) :
|
|
(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
}
|
|
|
|
this.serializeObject = (obj) => {
|
|
return protocol == protocols.json ? JSON.stringify(obj) : bson.serialize(obj);
|
|
}
|
|
|
|
this.deserializeMessage = (m) => {
|
|
return protocol == protocols.json ? JSON.parse(m) : bson.deserialize(m);
|
|
}
|
|
|
|
this.serializeInput = (input) => {
|
|
return protocol == protocols.json ?
|
|
((typeof input === "string" || input instanceof String) ? input : input.toString()) :
|
|
(Buffer.isBuffer(input) ? input : Buffer.from(input));
|
|
}
|
|
|
|
this.deserializeOutput = (content) => {
|
|
return protocol == protocols.json ? content : content.buffer;
|
|
}
|
|
|
|
// Used for generating strings to hold signature as js object keys.
|
|
this.stringifySignature = (sig) => {
|
|
if (typeof sig === 'string' || sig instanceof String)
|
|
return sig;
|
|
else if (sig instanceof Uint8Array)
|
|
return uint8ArrayToHex(sig);
|
|
else if (sig.buffer) // BSON binary.
|
|
return uint8ArrayToHex(new Uint8Array(sig.buffer));
|
|
else
|
|
throw "Cannot stringify signature.";
|
|
}
|
|
|
|
this.createUserChallengeResponse = (userChallenge, serverChallenge, msgProtocol) => {
|
|
// For challenge response encoding Hot Pocket always uses json.
|
|
// Challenge response will specify the protocol to use for contract messages.
|
|
const sigBytes = sodium.crypto_sign_detached(userChallenge, keys.privateKey);
|
|
|
|
return {
|
|
type: "user_challenge_response",
|
|
sig: this.binaryEncode(sigBytes),
|
|
pubkey: "ed" + this.binaryEncode(keys.publicKey),
|
|
server_challenge: serverChallenge,
|
|
protocol: msgProtocol
|
|
}
|
|
}
|
|
|
|
this.createContractInput = (input, nonce, maxLclSeqNo) => {
|
|
|
|
if (input.length == 0)
|
|
return null;
|
|
|
|
const inpContainer = {
|
|
input: this.serializeInput(input),
|
|
nonce: nonce,
|
|
max_lcl_seqno: maxLclSeqNo
|
|
}
|
|
|
|
const serlializedInpContainer = this.serializeObject(inpContainer);
|
|
const sigBytes = sodium.crypto_sign_detached(serlializedInpContainer, keys.privateKey);
|
|
|
|
const signedInpContainer = {
|
|
type: "contract_input",
|
|
input_container: serlializedInpContainer,
|
|
sig: this.binaryEncode(sigBytes)
|
|
}
|
|
|
|
return signedInpContainer;
|
|
}
|
|
|
|
this.createReadRequest = (request) => {
|
|
if (request.length == 0)
|
|
return null;
|
|
|
|
return {
|
|
type: "contract_read_request",
|
|
content: this.serializeInput(request)
|
|
}
|
|
}
|
|
|
|
this.createStatusRequest = () => {
|
|
return { type: "stat" };
|
|
}
|
|
}
|
|
|
|
function hexToUint8Array(hexString) {
|
|
return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
|
}
|
|
|
|
function uint8ArrayToHex(bytes) {
|
|
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
|
}
|
|
|
|
function EventEmitter() {
|
|
const registrations = {};
|
|
|
|
this.on = (eventName, listener) => {
|
|
if (!registrations[eventName])
|
|
registrations[eventName] = [];
|
|
registrations[eventName].push(listener);
|
|
}
|
|
|
|
this.emit = (eventName, ...value) => {
|
|
if (registrations[eventName])
|
|
registrations[eventName].forEach(listener => listener(...value));
|
|
}
|
|
|
|
this.clear = (eventName) => {
|
|
if (eventName)
|
|
delete registrations[eventName]
|
|
else
|
|
Object.keys(registrations).forEach(k => delete registrations[k]);
|
|
}
|
|
}
|
|
|
|
// Set sodium reference.
|
|
async function initSodium() {
|
|
if (sodium) // If already set, do nothing.
|
|
return;
|
|
else if (isBrowser && window.sodium) { // If no parameter specified, try to get from window.sodium.
|
|
sodium = window.sodium;
|
|
}
|
|
else if (isBrowser && !window.sodium) { // If sodium not yet loaded in browser, wait for sodium ready.
|
|
await new Promise(resolve => {
|
|
window.sodium = {
|
|
onload: async function (sodiumRef) {
|
|
sodium = sodiumRef;
|
|
resolve();
|
|
}
|
|
}
|
|
})
|
|
}
|
|
else if (!isBrowser) { // nodejs
|
|
sodium = require('libsodium-wrappers');
|
|
await sodium.ready;
|
|
}
|
|
}
|
|
|
|
// Set bson reference.
|
|
function initBson() {
|
|
if (bson) // If already set, do nothing.
|
|
return;
|
|
else if (isBrowser && window.BSON) // If no parameter specified, try to get from window.BSON.
|
|
bson = window.BSON;
|
|
else if (!isBrowser) // nodejs
|
|
bson = require('bson');
|
|
}
|
|
|
|
// Set WebSocket reference.
|
|
function initWebSocket() {
|
|
if (WebSocket) // If already set, do nothing.
|
|
return;
|
|
else if (isBrowser && window.WebSocket) // If no parameter specified, try to get from window.WebSocket.
|
|
WebSocket = window.WebSocket;
|
|
else if (!isBrowser) // nodejs
|
|
WebSocket = require('ws');
|
|
}
|
|
|
|
const hotPocketLib = {
|
|
generateKeys,
|
|
createClient,
|
|
events,
|
|
protocols
|
|
}
|
|
|
|
if (isBrowser)
|
|
window.HotPocket = hotPocketLib;
|
|
else
|
|
module.exports = hotPocketLib;
|
|
})(); |