UNPKG

reactotron-core-client

Version:

Grants Reactotron clients the ability to talk to a Reactotron server.

453 lines (406 loc) 13.3 kB
"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