UNPKG

@nodecg/types

Version:

Dynamic broadcast graphics rendered in a browser

607 lines (599 loc) 28.5 kB
const require_typed_emitter = require('../typed-emitter.js'); const require_api_base = require('../api.base.js'); const require_logger_interface = require('../logger-interface.js'); let express = require("express"); express = require_typed_emitter.__toESM(express); let is_error = require("is-error"); is_error = require_typed_emitter.__toESM(is_error); let serialize_error = require("serialize-error"); let node_fs = require("node:fs"); node_fs = require_typed_emitter.__toESM(node_fs); let node_path = require("node:path"); node_path = require_typed_emitter.__toESM(node_path); let __nodecg_internal_util = require("@nodecg/internal-util"); let yargs = require("yargs"); let cosmiconfig = require("cosmiconfig"); let klona_json = require("klona/json"); let zod = require("zod"); let __sentry_node = require("@sentry/node"); __sentry_node = require_typed_emitter.__toESM(__sentry_node); let node_util = require("node:util"); let winston = require("winston"); winston = require_typed_emitter.__toESM(winston); //#region src/types/nodecg-config-schema.ts const parsedArgv = zod.z.object({ bundlesEnabled: zod.z.string().optional().transform((val) => val?.split(",")), bundlesDisabled: zod.z.string().optional().transform((val) => val?.split(",")), bundlesPaths: zod.z.string().optional().transform((val) => val?.split(",")) }).parse(yargs.argv); const nodecgConfigSchema = zod.z.object({ host: zod.z.string().default("0.0.0.0").describe("The IP address or hostname that NodeCG should bind to."), port: zod.z.number().int().positive().default(9090).describe("The port that NodeCG should listen on."), baseURL: zod.z.string().optional().describe("The URL of this instance. Used for things like cookies. Defaults to HOST:PORT. If you use a reverse proxy, you'll likely need to set this value."), exitOnUncaught: zod.z.boolean().default(true).describe("Whether or not to exit on uncaught exceptions."), logging: zod.z.object({ console: zod.z.object({ enabled: zod.z.boolean().default(true).describe("Whether to enable console logging."), level: zod.z.enum(require_logger_interface.LogLevels).default("info").describe("The log level to use."), timestamps: zod.z.boolean().default(true).describe("Whether to add timestamps to the console logging."), replicants: zod.z.boolean().default(false).describe("Whether to enable logging of the Replicants subsystem. Very spammy.") }).default({ enabled: true }), file: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Whether to enable file logging."), level: zod.z.enum(require_logger_interface.LogLevels).default("info").describe("The log level to use."), path: zod.z.string().default("logs/nodecg.log").describe("The filepath to log to."), timestamps: zod.z.boolean().default(true).describe("Whether to add timestamps to the file logging."), replicants: zod.z.boolean().default(false).describe("Whether to enable logging of the Replicants subsystem. Very spammy.") }).default({ enabled: false }) }).default({ console: {} }), bundles: zod.z.object({ enabled: zod.z.array(zod.z.string()).nullable().default(parsedArgv.bundlesEnabled ?? null).describe("A whitelist array of bundle names."), disabled: zod.z.array(zod.z.string()).nullable().default(parsedArgv.bundlesDisabled ?? null).describe("A blacklist array of bundle names."), paths: zod.z.array(zod.z.string()).default(parsedArgv.bundlesPaths ?? []).describe("An array of additional paths where bundles are located.") }).default({ enabled: null, disabled: null, paths: [] }), login: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Whether to enable login security."), sessionSecret: zod.z.string().optional().describe("The secret used to salt sessions."), forceHttpsReturn: zod.z.boolean().default(false).describe("Forces Steam & Twitch login return URLs to use HTTPS instead of HTTP. Useful in reverse proxy setups."), steam: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Whether to enable Steam authentication."), apiKey: zod.z.string().optional().describe("A Steam API Key. Obtained from http://steamcommunity.com/dev/apikey"), allowedIds: zod.z.array(zod.z.string()).default([]).describe("Which 64 bit Steam IDs to allow. Can be obtained from https://steamid.io/") }).optional().refine((val) => val?.enabled ? typeof val.apiKey === "string" : true, { message: "\"login.steam.apiKey\" must be a string" }), twitch: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Whether to enable Twitch authentication."), clientID: zod.z.string().optional().describe("A Twitch application ClientID http://twitch.tv/kraken/oauth2/clients/new"), clientSecret: zod.z.string().optional().describe("A Twitch application ClientSecret http://twitch.tv/kraken/oauth2/clients/new"), scope: zod.z.string().default("user_read").describe("A space-separated string of Twitch application permissions."), allowedUsernames: zod.z.array(zod.z.string()).default([]).describe("Which Twitch usernames to allow."), allowedIds: zod.z.array(zod.z.string()).default([]).describe("Which Twitch IDs to allow. Can be obtained from https://twitchinsights.net/checkuser") }).optional().refine((val) => val?.enabled ? typeof val.clientID === "string" : true, { message: "\"login.twitch.clientID\" must be a string" }).refine((val) => val?.enabled ? typeof val.clientSecret === "string" : true, { message: "\"login.twitch.clientID\" must be a string" }), discord: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Whether to enable Discord authentication."), clientID: zod.z.string().optional().describe("A Discord application ClientID https://discord.com/developers/applications"), clientSecret: zod.z.string().optional().describe("A Discord application ClientSecret https://discord.com/developers/applications"), scope: zod.z.string().default("identify").describe("A space-separated string of Discord application scopes. https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes"), allowedUserIDs: zod.z.array(zod.z.string()).default([]).describe("Which Discord user IDs to allow."), allowedGuilds: zod.z.array(zod.z.object({ guildID: zod.z.string().describe("Users in this Discord Server are allowed to log in."), allowedRoleIDs: zod.z.array(zod.z.string()).default([]).describe("Additionally require one of the roles on the server to log in."), guildBotToken: zod.z.string().default("").describe("Discord bot token, needed if allowedRoleIDs is used.") })).default([]) }).optional().refine((val) => val?.enabled ? typeof val.clientID === "string" : true, { message: "\"login.discord.clientID\" must be a string" }).refine((val) => val?.enabled ? typeof val.clientSecret === "string" : true, { message: "\"login.discord.clientSecret\" must be a string" }), local: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Enable Local authentication."), allowedUsers: zod.z.array(zod.z.object({ username: zod.z.string(), password: zod.z.string() })).default([]).describe("Which users can log in.") }).optional() }).refine((val) => val.enabled ? typeof val.sessionSecret === "string" : true, { message: "\"login.sessionSecret\" must be a string" }).default({ enabled: false }), ssl: zod.z.object({ enabled: zod.z.boolean().default(false).describe("Whether to enable SSL/HTTPS encryption."), allowHTTP: zod.z.boolean().default(false).describe("Whether to allow insecure HTTP connections while SSL is active."), keyPath: zod.z.string().optional().describe("The path to an SSL key file."), certificatePath: zod.z.string().optional().describe("The path to an SSL certificate file."), passphrase: zod.z.string().optional().describe("The passphrase for the provided key file.") }).default({ enabled: false }).refine((val) => val.enabled ? typeof val.keyPath === "string" : true, { message: "\"ssl.keyPath\" must be a string" }).refine((val) => val.enabled ? typeof val.certificatePath === "string" : true, { message: "\"ssl.certificatePath\" must be a string" }), sentry: zod.z.object({ enabled: zod.z.boolean().default(true).describe("Whether to enable Sentry error reporting."), dsn: zod.z.string().optional().describe("Your project's DSN, used to route alerts to the correct place.") }).default({ enabled: false }).refine((val) => val.enabled ? typeof val.dsn === "string" : true, { message: "\"sentry.dsn\" must be a string" }) }).transform((val) => { const host = val.host === "0.0.0.0" ? "localhost" : val.host; return { ...val, baseURL: val.baseURL ?? `${host}:${val.port}` }; }); //#endregion //#region src/server/config/loader.ts const loadConfig = (cfgDirOrFile) => { let isFile = false; try { isFile = node_fs.lstatSync(cfgDirOrFile).isFile(); } catch (error) { if (error.code !== "ENOENT") throw error; } const cfgDir = isFile ? node_path.dirname(cfgDirOrFile) : cfgDirOrFile; const userCfg = (0, cosmiconfig.cosmiconfigSync)("nodecg", { searchPlaces: isFile ? [node_path.basename(cfgDirOrFile)] : [ "nodecg.json", "nodecg.yaml", "nodecg.yml", "nodecg.js", "nodecg.config.js" ], stopDir: cfgDir }).search(cfgDir)?.config ?? {}; if (userCfg?.bundles?.enabled && userCfg?.bundles?.disabled) throw new Error("nodecg.json may only contain EITHER bundles.enabled OR bundles.disabled, not both."); else if (!userCfg) console.info("[nodecg] No config found, using defaults."); const parseResult = nodecgConfigSchema.safeParse(userCfg); if (!parseResult.success) { console.error("[nodecg] Config invalid:", parseResult.error.errors[0]?.message); throw new Error(parseResult.error.errors[0]?.message); } const config$1 = parseResult.data; const filteredConfig$1 = { host: config$1.host, port: config$1.port, baseURL: config$1.baseURL, logging: { console: { enabled: config$1.logging.console.enabled, level: config$1.logging.console.level, timestamps: config$1.logging.console.timestamps, replicants: config$1.logging.console.replicants }, file: { enabled: config$1.logging.file.enabled, level: config$1.logging.file.level, timestamps: config$1.logging.file.timestamps, replicants: config$1.logging.file.replicants } }, login: { enabled: config$1.login.enabled }, sentry: { enabled: config$1.sentry.enabled, dsn: config$1.sentry.enabled ? config$1.sentry.dsn : void 0 } }; if (config$1.login.enabled && config$1.login.steam) filteredConfig$1.login.steam = { enabled: config$1.login.steam.enabled }; if (config$1.login.enabled && config$1.login.twitch) filteredConfig$1.login.twitch = { enabled: config$1.login.twitch.enabled, clientID: config$1.login.twitch.enabled ? config$1.login.twitch.clientID : void 0, scope: config$1.login.twitch.enabled ? config$1.login.twitch.scope : void 0 }; if (config$1.login.enabled && config$1.login.local) filteredConfig$1.login.local = { enabled: config$1.login.local.enabled }; if (config$1.login.enabled && config$1.login.discord) filteredConfig$1.login.discord = { enabled: config$1.login.discord.enabled, clientID: config$1.login.discord.enabled ? config$1.login.discord.clientID : void 0, scope: config$1.login.discord.enabled ? config$1.login.discord.scope : void 0 }; if (config$1.ssl) filteredConfig$1.ssl = { enabled: config$1.ssl.enabled }; return { config: (0, klona_json.klona)(config$1), filteredConfig: (0, klona_json.klona)(filteredConfig$1) }; }; //#endregion //#region src/server/config/index.ts const cfgDirectoryPath = yargs.argv.cfgPath ?? node_path.join(__nodecg_internal_util.rootPaths.getRuntimeRoot(), "cfg"); if (!node_fs.existsSync(cfgDirectoryPath)) node_fs.mkdirSync(cfgDirectoryPath, { recursive: true }); const { config, filteredConfig } = loadConfig(cfgDirectoryPath); const exitOnUncaught = config.exitOnUncaught; const sentryEnabled = config.sentry?.enabled; //#endregion //#region src/server/logger/logger.server.ts /** * A factory that configures and returns a Logger constructor. * * @returns A constructor used to create discrete logger instances. */ function loggerFactory(initialOpts = {}, sentry = void 0) { initialOpts = initialOpts || {}; initialOpts.console = initialOpts.console ?? {}; initialOpts.file = initialOpts.file ?? {}; initialOpts.file.path = initialOpts.file.path ?? "logs/nodecg.log"; const consoleTransport = new winston.default.transports.Console({ level: initialOpts.console.level ?? require_logger_interface.LogLevel.Info, silent: !initialOpts.console.enabled, stderrLevels: ["warn", "error"], format: winston.default.format.combine(winston.default.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.default.format.errors({ stack: true }), winston.default.format.colorize(), winston.default.format.printf((info) => `${initialOpts?.console?.timestamps ? `${info.timestamp} - ` : ""}${info.level}: ${info.message}`)) }); const fileTransport = new winston.default.transports.File({ filename: initialOpts.file.path, level: initialOpts.file.level ?? require_logger_interface.LogLevel.Info, silent: !initialOpts.file.enabled, format: winston.default.format.combine(winston.default.format.timestamp(), winston.default.format.errors({ stack: true }), winston.default.format.printf((info) => `${initialOpts?.file?.timestamps ? `${info.timestamp} - ` : ""}${info.level}: ${info.message}`)) }); if (typeof initialOpts.file.path !== "undefined") { fileTransport.filename = initialOpts.file.path; if (!node_fs.default.existsSync(node_path.dirname(initialOpts.file.path))) node_fs.default.mkdirSync(node_path.dirname(initialOpts.file.path), { recursive: true }); } winston.default.addColors({ verbose: "green", debug: "cyan", info: "white", warn: "yellow", error: "red" }); const consoleLogger = winston.default.createLogger({ transports: [consoleTransport], levels: { verbose: 4, trace: 4, debug: 3, info: 2, warn: 1, error: 0 } }); const fileLogger = winston.default.createLogger({ transports: [fileTransport], levels: { verbose: 4, trace: 4, debug: 3, info: 2, warn: 1, error: 0 } }); /** * Constructs a new Logger instance that prefixes all output with the given name. * @param name {String} - The label to prefix all output of this logger with. * @returns {Object} - A Logger instance. * @constructor */ return class Logger$1 { static _consoleLogger = consoleLogger; static _fileLogger = fileLogger; static _shouldConsoleLogReplicants = Boolean(initialOpts.console?.replicants); static _shouldFileLogReplicants = Boolean(initialOpts.file?.replicants); constructor(name) { this.name = name; this.name = name; } trace(...args) { [consoleLogger, fileLogger].forEach((logger) => logger.verbose(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`)); } debug(...args) { [consoleLogger, fileLogger].forEach((logger) => logger.debug(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`)); } info(...args) { [consoleLogger, fileLogger].forEach((logger) => logger.info(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`)); } warn(...args) { [consoleLogger, fileLogger].forEach((logger) => logger.warn(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`)); } error(...args) { [consoleLogger, fileLogger].forEach((logger) => logger.error(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`)); if (sentry) { const formattedArgs = args.map((argument) => typeof argument === "object" ? (0, node_util.inspect)(argument, { depth: null, showProxy: true }) : argument); sentry.captureException(/* @__PURE__ */ new Error(`[${this.name}] ` + (0, node_util.format)(formattedArgs[0], ...formattedArgs.slice(1)))); } } replicants(...args) { if (Logger$1._shouldConsoleLogReplicants) consoleLogger.info(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`); if (Logger$1._shouldFileLogReplicants) fileLogger.info(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`); } }; } //#endregion //#region src/server/logger/index.ts let Logger; if (config.sentry?.enabled) Logger = loggerFactory(config.logging, __sentry_node); else Logger = loggerFactory(config.logging); //#endregion //#region src/server/util/authcheck.ts /** * Express middleware that checks if the user is authenticated. */ const authCheck = async (req, res, next) => { try { if (!config.login?.enabled) { next(); return; } let { user } = req; let isUsingKeyOrSocketToken = false; let keyOrSocketTokenAuthenticated = false; if (req.query.key ?? req.cookies.socketToken) { isUsingKeyOrSocketToken = true; const apiKey = await res.locals.databaseAdapter.findApiKey(req.query.key ?? req.cookies.socketToken); if (!apiKey) { req.session?.destroy(() => { res.clearCookie("socketToken", { secure: req.secure, sameSite: req.secure ? "none" : void 0 }); res.clearCookie("connect.sid", { path: "/" }); res.clearCookie("io", { path: "/" }); res.redirect("/login"); }); return; } user = await res.locals.databaseAdapter.findUser(apiKey.user.id) ?? void 0; } if (!user) { if (req.session) req.session.returnTo = req.url; res.status(403).redirect("/login"); return; } const allowed = res.locals.databaseAdapter.isSuperUser(user); keyOrSocketTokenAuthenticated = isUsingKeyOrSocketToken && allowed; const provider = user.identities[0].provider_type; const providerAllowed = config.login?.[provider]?.enabled; if ((keyOrSocketTokenAuthenticated || req.isAuthenticated()) && allowed && providerAllowed) { let apiKey = user.apiKeys[0]; if (!apiKey) { apiKey = await res.locals.databaseAdapter.createApiKey(); user.apiKeys.push(apiKey); await res.locals.databaseAdapter.saveUser(user); } res.cookie("socketToken", apiKey.secret_key, { secure: req.secure, sameSite: req.secure ? "none" : void 0 }); next(); return; } if (req.session) req.session.returnTo = req.url; res.status(403).redirect("/login"); return; } catch (error) { next(error); } }; //#endregion //#region src/server/api.server.ts function serverApiFactory(io, replicator, extensions, mount) { const apiContexts = /* @__PURE__ */ new Set(); /** * This is what enables intra-context messaging. * I.e., passing messages from one extension to another in the same Node.js context. */ function _forwardMessageToContext(messageName, bundleName, data) { process.nextTick(() => { apiContexts.forEach((ctx) => { ctx._messageHandlers.forEach((handler) => { if (messageName === handler.messageName && bundleName === handler.bundleName) handler.func(data); }); }); }); } return class NodeCGAPIServer extends require_api_base.NodeCGAPIBase { static sendMessageToBundle(messageName, bundleName, data) { _forwardMessageToContext(messageName, bundleName, data); io.emit("message", { bundleName, messageName, content: data }); } static readReplicant(name, namespace) { if (!name || typeof name !== "string") throw new Error("Must supply a name when reading a Replicant"); if (!namespace || typeof namespace !== "string") throw new Error("Must supply a namespace when reading a Replicant"); return replicator.declare(name, namespace).value; } static Replicant(name, namespace, opts) { if (!name || typeof name !== "string") throw new Error("Must supply a name when reading a Replicant"); if (!namespace || typeof namespace !== "string") throw new Error("Must supply a namespace when reading a Replicant"); return replicator.declare(name, namespace, opts); } Logger = Logger; log = new Logger(this.bundleName); /** * The full NodeCG server config, including potentially sensitive keys. */ config = JSON.parse(JSON.stringify(config)); /** * _Extension only_<br/> * Creates a new express router. * See the [express docs](http://expressjs.com/en/api.html#express.router) for usage. * @function */ Router = express.default.Router; util = { authCheck }; /** * _Extension only_<br/> * Object containing references to all other loaded extensions. To access another bundle's extension, * it _must_ be declared as a `bundleDependency` in your bundle's [`package.json`]{@tutorial manifest}. * @name NodeCG#extensions * * @example * // bundles/my-bundle/package.json * { * "name": "my-bundle" * ... * "bundleDependencies": { * "other-bundle": "^1.0.0" * } * } * * // bundles/my-bundle/extension.js * module.exports = function (nodecg) { * const otherBundle = nodecg.extensions['other-bundle']; * // Now I can use `otherBundle`! * } */ extensions = extensions; /** * _Extension only_<br/> * Mounts Express middleware to the main server Express app. * Middleware mounted using this method comes _after_ all the middlware that NodeCG * uses internally. * See the [Express docs](http://expressjs.com/en/api.html#app.use) for usage. * @function */ mount = mount; constructor(bundle) { super(bundle); apiContexts.add(this); io.on("connection", (socket) => { socket.on("message", (data, ack) => { const wrappedAck = _wrapAcknowledgement(ack); this._messageHandlers.forEach((handler) => { if (data.messageName === handler.messageName && data.bundleName === handler.bundleName) handler.func(data.content, wrappedAck); }); }); }); } /** * _Extension only_<br/> * Gets the server Socket.IO context. * @function */ getSocketIOServer = () => io; /** * Sends a message to a specific bundle. Also available as a static method. * See {@link NodeCG#sendMessage} for usage details. * @param {string} messageName - The name of the message. * @param {string} bundleName - The name of the target bundle. * @param {mixed} [data] - The data to send. * @param {function} [cb] - _Browser only_ The error-first callback to handle the server's * [acknowledgement](http://socket.io/docs/#sending-and-getting-data-%28acknowledgements%29) message, if any. * @return {Promise|undefined} - _Browser only_ A Promise that is rejected if the first argument provided to the * acknowledgement is an `Error`, otherwise it is resolved with the remaining arguments provided to the acknowledgement. * But, if a callback was provided, this return value will be `undefined`, and there will be no Promise. */ sendMessageToBundle(messageName, bundleName, data) { this.log.trace("Sending message %s to bundle %s with data:", messageName, bundleName, data); return NodeCGAPIServer.sendMessageToBundle.apply(require_api_base.NodeCGAPIBase, arguments); } /** * Sends a message with optional data within the current bundle. * Messages can be sent from client to server, server to client, or client to client. * * Messages are namespaced by bundle. To send a message in another bundle's namespace, * use {@link NodeCG#sendMessageToBundle}. * * When a `sendMessage` is used from a client context (i.e., graphic or dashboard panel), * it returns a `Promise` called an "acknowledgement". Your server-side code (i.e., extension) * can invoke this acknowledgement with whatever data (or error) it wants. Errors sent to acknowledgements * from the server will be properly serialized and intact when received on the client. * * Alternatively, if you do not wish to use a `Promise`, you can provide a standard error-first * callback as the last argument to `sendMessage`. * * If your server-side code has multiple listenFor handlers for your message, * you must first check if the acknowledgement has already been handled before * attempting to call it. You may so do by checking the `.handled` boolean * property of the `ack` function passed to your listenFor handler. * * See [Socket.IO's docs](http://socket.io/docs/#sending-and-getting-data-%28acknowledgements%29) * for more information on how acknowledgements work under the hood. * * @param {string} messageName - The name of the message. * @param {mixed} [data] - The data to send. * @param {function} [cb] - _Browser only_ The error-first callback to handle the server's * [acknowledgement](http://socket.io/docs/#sending-and-getting-data-%28acknowledgements%29) message, if any. * @return {Promise} - _Browser only_ A Promise that is rejected if the first argument provided to the * acknowledgement is an `Error`, otherwise it is resolved with the remaining arguments provided to the acknowledgement. * * @example <caption>Sending a normal message:</caption> * nodecg.sendMessage('printMessage', 'dope.'); * * @example <caption>Sending a message and replying with an acknowledgement:</caption> * // bundles/my-bundle/extension.js * module.exports = function (nodecg) { * nodecg.listenFor('multiplyByTwo', (value, ack) => { * if (value === 4) { * ack(new Error('I don\'t like multiplying the number 4!'); * return; * } * * // acknowledgements should always be error-first callbacks. * // If you do not wish to send an error, send "null" * if (ack && !ack.handled) { * ack(null, value * 2); * } * }); * } * * // bundles/my-bundle/graphics/script.js * // Both of these examples are functionally identical. * * // Promise acknowledgement * nodecg.sendMessage('multiplyByTwo', 2) * .then(result => { * console.log(result); // Will eventually print '4' * .catch(error => { * console.error(error); * }); * * // Error-first callback acknowledgement * nodecg.sendMessage('multiplyByTwo', 2, (error, result) => { * if (error) { * console.error(error); * return; * } * * console.log(result); // Will eventually print '4' * }); */ sendMessage(messageName, data) { this.sendMessageToBundle(messageName, this.bundleName, data); } /** * Reads the value of a replicant once, and doesn't create a subscription to it. Also available as a static method. * @param {string} name - The name of the replicant. * @param {string} [bundle=CURR_BNDL] - The bundle namespace to in which to look for this replicant. * @param {function} cb - _Browser only_ The callback that handles the server's response which contains the value. * @example <caption>From an extension:</caption> * // Extensions have immediate access to the database of Replicants. * // For this reason, they can use readReplicant synchronously, without a callback. * module.exports = function (nodecg) { * var myVal = nodecg.readReplicant('myVar', 'some-bundle'); * } * @example <caption>From a graphic or dashboard panel:</caption> * // Graphics and dashboard panels must query the server to retrieve the value, * // and therefore must provide a callback. * nodecg.readReplicant('myRep', 'some-bundle', value => { * // I can use 'value' now! * console.log('myRep has the value '+ value +'!'); * }); */ readReplicant(name, param2) { let { bundleName } = this; if (typeof param2 === "string") bundleName = param2; else if (typeof param2 === "object" && bundleName in param2) bundleName = param2.name; return this.constructor.readReplicant(name, bundleName); } _replicantFactory = (name, namespace, opts) => replicator.declare(name, namespace, opts); }; } /** * By default, Errors get serialized to empty objects when run through JSON.stringify. * This function wraps an "acknowledgement" callback and checks if the first argument * is an Error. If it is, that Error is serialized _before_ being sent off to Socket.IO * for serialization to be sent across the wire. * @param ack {Function} * @private * @ignore * @returns {Function} */ function _wrapAcknowledgement(ack) { let handled = false; const wrappedAck = function(firstArg, ...restArgs) { if (handled) throw new Error("Acknowledgement already handled"); handled = true; if ((0, is_error.default)(firstArg)) firstArg = (0, serialize_error.serializeError)(firstArg); ack(firstArg, ...restArgs); }; Object.defineProperty(wrappedAck, "handled", { get() { return handled; } }); return wrappedAck; } //#endregion exports.serverApiFactory = serverApiFactory; //# sourceMappingURL=api.server.js.map