@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
300 lines • 13.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.serverApiFactory = serverApiFactory;
const express_1 = __importDefault(require("express"));
const is_error_1 = __importDefault(require("is-error"));
const serialize_error_1 = require("serialize-error");
const api_base_1 = require("../shared/api.base");
const config_1 = require("./config");
const logger_1 = require("./logger");
const authcheck_1 = require("./util/authcheck");
function serverApiFactory(io, replicator, extensions, mount) {
const apiContexts = 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 api_base_1.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");
}
const replicant = replicator.declare(name, namespace);
return replicant.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);
}
get Logger() {
return logger_1.Logger;
}
get log() {
if (this._memoizedLogger) {
return this._memoizedLogger;
}
this._memoizedLogger = new logger_1.Logger(this.bundleName);
return this._memoizedLogger;
}
/**
* The full NodeCG server config, including potentially sensitive keys.
*/
get config() {
return JSON.parse(JSON.stringify(config_1.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_1.default.Router;
util = {
/**
* _Extension only_<br/>
* Checks if a session is authorized. Intended to be used in express routes.
* @param {object} req - A HTTP request.
* @param {object} res - A HTTP response.
* @param {function} next - The next middleware in the control flow.
*/
authCheck: authcheck_1.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`!
* }
*/
get extensions() {
return 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;
_memoizedLogger;
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(api_base_1.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_1.default)(firstArg)) {
firstArg = (0, serialize_error_1.serializeError)(firstArg);
}
ack(firstArg, ...restArgs);
};
Object.defineProperty(wrappedAck, "handled", {
get() {
return handled;
},
});
return wrappedAck;
}
//# sourceMappingURL=api.server.js.map