@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
607 lines (599 loc) • 28.5 kB
JavaScript
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