reactotron-core-client
Version:
Grants Reactotron clients the ability to talk to a Reactotron server.
453 lines (406 loc) • 13.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ReactotronImpl = exports.ArgType = void 0;
Object.defineProperty(exports, "assertHasLoggerPlugin", {
enumerable: true,
get: function () {
return _logger.assertHasLoggerPlugin;
}
});
Object.defineProperty(exports, "assertHasStateResponsePlugin", {
enumerable: true,
get: function () {
return _stateResponses.assertHasStateResponsePlugin;
}
});
exports.corePlugins = void 0;
exports.createClient = createClient;
Object.defineProperty(exports, "hasStateResponsePlugin", {
enumerable: true,
get: function () {
return _stateResponses.hasStateResponsePlugin;
}
});
var _validate = _interopRequireDefault(require("./validate"));
var _logger = _interopRequireWildcard(require("./plugins/logger"));
var _image = _interopRequireDefault(require("./plugins/image"));
var _benchmark = _interopRequireDefault(require("./plugins/benchmark"));
var _stateResponses = _interopRequireWildcard(require("./plugins/state-responses"));
var _apiResponse = _interopRequireDefault(require("./plugins/api-response"));
var _clear = _interopRequireDefault(require("./plugins/clear"));
var _repl = _interopRequireDefault(require("./plugins/repl"));
var _serialize = _interopRequireDefault(require("./serialize"));
var _stopwatch = require("./stopwatch");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
let ArgType = exports.ArgType = /*#__PURE__*/function (ArgType) {
ArgType["String"] = "string";
return ArgType;
}({}); // #region Plugin Types
const corePlugins = exports.corePlugins = [(0, _image.default)(), (0, _logger.default)(), (0, _benchmark.default)(), (0, _stateResponses.default)(), (0, _apiResponse.default)(), (0, _clear.default)(), (0, _repl.default)()];
// #endregion
// these are not for you.
const reservedFeatures = ["configure", "connect", "connected", "options", "plugins", "send", "socket", "startTimer", "use"];
const isReservedFeature = value => reservedFeatures.some(res => res === value);
function emptyPromise() {
return Promise.resolve("");
}
class ReactotronImpl {
// the configuration options
/**
* Are we connected to a server?
*/
connected = false;
/**
* The socket we're using.
*/
socket = null;
/**
* Available plugins.
*/
plugins = [];
/**
* Messages that need to be sent.
*/
sendQueue = [];
/**
* Are we ready to start communicating?
*/
isReady = false;
/**
* The last time we sent a message.
*/
lastMessageDate = new Date();
/**
* The registered custom commands
*/
customCommands = [];
/**
* The current ID for custom commands
*/
customCommandLatestId = 1;
/**
* Starts a timer and returns a function you can call to stop it and return the elapsed time.
*/
startTimer = () => (0, _stopwatch.start)();
/**
* Set the configuration options.
*/
configure(options) {
// options get merged & validated before getting set
const newOptions = Object.assign({
createSocket: null,
host: "localhost",
port: 9090,
name: "reactotron-core-client",
secure: false,
plugins: corePlugins,
safeRecursion: true,
onCommand: () => null,
onConnect: () => null,
onDisconnect: () => null
}, this.options, options);
(0, _validate.default)(newOptions);
this.options = newOptions;
// if we have plugins, let's add them here
if (Array.isArray(this.options.plugins)) {
this.options.plugins.forEach(p => this.use(p));
}
return this;
}
close() {
this.connected = false;
this.socket && this.socket.close && this.socket.close();
}
/**
* Connect to the Reactotron server.
*/
connect() {
this.connected = true;
const {
createSocket,
secure,
host,
environment,
port,
name,
client = {},
getClientId
} = this.options;
const {
onCommand,
onConnect,
onDisconnect
} = this.options;
// establish a connection to the server
const protocol = secure ? "wss" : "ws";
const socket = createSocket(`${protocol}://${host}:${port}`);
// fires when we talk to the server
const onOpen = () => {
// fire our optional onConnect handler
onConnect && onConnect();
// trigger our plugins onConnect
this.plugins.forEach(p => p.onConnect && p.onConnect());
const getClientIdPromise = getClientId || emptyPromise;
getClientIdPromise(name).then(clientId => {
this.isReady = true;
// introduce ourselves
this.send("client.intro", {
environment,
...client,
name: name,
clientId,
reactotronCoreClientVersion: "REACTOTRON_CORE_CLIENT_VERSION"
});
// flush the send queue
while (this.sendQueue.length > 0) {
const h = this.sendQueue[0];
this.sendQueue = this.sendQueue.slice(1);
this.socket.send(h);
}
});
};
// fires when we disconnect
const onClose = () => {
this.isReady = false;
// trigger our disconnect handler
onDisconnect && onDisconnect();
// as well as the plugin's onDisconnect
this.plugins.forEach(p => p.onDisconnect && p.onDisconnect());
};
const decodeCommandData = data => {
if (typeof data === "string") {
return JSON.parse(data);
}
if (Buffer.isBuffer(data)) {
return JSON.parse(data.toString());
}
return data;
};
// fires when we receive a command, just forward it off
const onMessage = data => {
const command = decodeCommandData(data);
// trigger our own command handler
onCommand && onCommand(command);
// trigger our plugins onCommand
this.plugins.forEach(p => p.onCommand && p.onCommand(command));
// trigger our registered custom commands
if (command.type === "custom") {
this.customCommands.filter(cc => {
if (typeof command.payload === "string") {
return cc.command === command.payload;
}
return cc.command === command.payload.command;
}).forEach(cc => cc.handler(typeof command.payload === "object" ? command.payload.args : undefined));
} else if (command.type === "setClientId") {
this.options.setClientId && this.options.setClientId(command.payload);
}
};
// this is ws style from require('ws') on node js
if ("on" in socket && socket.on) {
const nodeWebSocket = socket;
nodeWebSocket.on("open", onOpen);
nodeWebSocket.on("close", onClose);
nodeWebSocket.on("message", onMessage);
// assign the socket to the instance
this.socket = socket;
} else {
// this is a browser
const browserWebSocket = socket;
socket.onopen = onOpen;
socket.onclose = onClose;
socket.onmessage = evt => onMessage(evt.data);
// assign the socket to the instance
this.socket = browserWebSocket;
}
return this;
}
/**
* Sends a command to the server
*/
send = (type, payload, important) => {
// set the timing info
const date = new Date();
let deltaTime = date.getTime() - this.lastMessageDate.getTime();
// glitches in the matrix
if (deltaTime < 0) {
deltaTime = 0;
}
this.lastMessageDate = date;
const fullMessage = {
type,
payload,
important: !!important,
date: date.toISOString(),
deltaTime
};
const serializedMessage = (0, _serialize.default)(fullMessage, this.options.proxyHack);
if (this.isReady) {
// send this command
try {
this.socket.send(serializedMessage);
} catch {
this.isReady = false;
console.log("An error occurred communicating with reactotron. Please reload your app");
}
} else {
// queue it up until we can connect
this.sendQueue.push(serializedMessage);
}
};
/**
* Sends a custom command to the server to displays nicely.
*/
display(config) {
const {
name,
value,
preview,
image: img,
important = false
} = config;
const payload = {
name,
value: value || undefined,
preview: preview || undefined,
image: img || undefined
};
this.send("display", payload, important);
}
/**
* Client libraries can hijack this to report errors.
*/
reportError(error) {
this.error(error);
}
/**
* Adds a plugin to the system
*/
use(pluginCreator) {
// we're supposed to be given a function
if (typeof pluginCreator !== "function") {
throw new Error("plugins must be a function");
}
// execute it immediately passing the send function
const plugin = pluginCreator.bind(this)(this);
// ensure we get an Object-like creature back
if (typeof plugin !== "object") {
throw new Error("plugins must return an object");
}
// do we have features to mixin?
if (plugin.features) {
// validate
if (typeof plugin.features !== "object") {
throw new Error("features must be an object");
}
// here's how we're going to inject these in
const inject = key => {
// grab the function
const featureFunction = plugin.features[key];
// only functions may pass
if (typeof featureFunction !== "function") {
throw new Error(`feature ${key} is not a function`);
}
// ditch reserved names
if (isReservedFeature(key)) {
throw new Error(`feature ${key} is a reserved name`);
}
// ok, let's glue it up... and lose all respect from elite JS champions.
this[key] = featureFunction;
};
// let's inject
Object.keys(plugin.features).forEach(key => inject(key));
}
// add it to the list
this.plugins.push(plugin);
// call the plugins onPlugin
plugin.onPlugin && typeof plugin.onPlugin === "function" && plugin.onPlugin.bind(this)(this);
// chain-friendly
return this;
}
onCustomCommand(config, optHandler) {
let command;
let handler;
let title;
let description;
let args;
if (typeof config === "string") {
command = config;
handler = optHandler;
} else {
command = config.command;
handler = config.handler;
title = config.title;
description = config.description;
args = config.args;
}
// Validations
// Make sure there is a command
if (!command) {
throw new Error("A command is required");
}
// Make sure there is a handler
if (!handler) {
throw new Error(`A handler is required for command "${command}"`);
}
// Make sure the command doesn't already exist
const existingCommands = this.customCommands.filter(cc => cc.command === command);
if (existingCommands.length > 0) {
existingCommands.forEach(command => {
this.customCommands = this.customCommands.filter(cc => cc.id !== command.id);
this.send("customCommand.unregister", {
id: command.id,
command: command.command
});
});
}
if (args) {
const argNames = [];
args.forEach(arg => {
if (!arg.name) {
throw new Error(`A arg on the command "${command}" is missing a name`);
}
if (argNames.indexOf(arg.name) > -1) {
throw new Error(`A arg with the name "${arg.name}" already exists in the command "${command}"`);
}
argNames.push(arg.name);
});
}
// Create this command handlers object
const customHandler = {
id: this.customCommandLatestId,
command,
handler,
title,
description,
args
};
// Increment our id counter
this.customCommandLatestId += 1;
// Add it to our array
this.customCommands.push(customHandler);
this.send("customCommand.register", {
id: customHandler.id,
command: customHandler.command,
title: customHandler.title,
description: customHandler.description,
args: customHandler.args
});
return () => {
this.customCommands = this.customCommands.filter(cc => cc.id !== customHandler.id);
this.send("customCommand.unregister", {
id: customHandler.id,
command: customHandler.command
});
};
}
}
// convenience factory function
exports.ReactotronImpl = ReactotronImpl;
function createClient(options) {
const client = new ReactotronImpl();
return client.configure(options);
}
//# sourceMappingURL=reactotron-core-client.js.map