@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
515 lines (510 loc) • 18.4 kB
JavaScript
const require_typed_emitter = require('./typed-emitter.js');
let klona_json = require("klona/json");
let object_path = require("object-path");
object_path = require_typed_emitter.__toESM(object_path);
require("@nodecg/json-schema-defaults");
let ajv = require("ajv");
ajv = require_typed_emitter.__toESM(ajv);
let ajv_dist_2019 = require("ajv/dist/2019");
ajv_dist_2019 = require_typed_emitter.__toESM(ajv_dist_2019);
let ajv_dist_2020 = require("ajv/dist/2020");
ajv_dist_2020 = require_typed_emitter.__toESM(ajv_dist_2020);
let ajv_draft_04 = require("ajv-draft-04");
ajv_draft_04 = require_typed_emitter.__toESM(ajv_draft_04);
let ajv_formats = require("ajv-formats");
ajv_formats = require_typed_emitter.__toESM(ajv_formats);
require("fp-ts/Either");
//#region src/shared/utils/errors.ts
/**
* Make a string out of an error (or other equivalents),
* including any additional data such as stack trace if available.
* Safe to use on unknown inputs.
*/
function stringifyError(error, noStack = false) {
const o = stringifyErrorInner(error);
if (noStack || !o.stack) return o.message;
return `${o.message}, ${o.stack}`;
}
function stringifyErrorInner(error) {
let message;
let stack;
if (typeof error === "string") message = error;
else if (error === null) message = "null";
else if (error === void 0) message = "undefined";
else if (error && typeof error === "object") if (typeof error.error === "object" && error.error.message) {
message = error.error.message;
stack = error.error.stack;
} else if (error.reason) if (error.reason.message) {
message = error.reason.message;
stack = error.reason.stack || error.reason.reason;
} else {
message = error.reason;
stack = error.stack;
}
else if (error.message) {
message = error.message;
stack = error.stack;
} else if (error.details) message = error.details;
else try {
message = JSON.stringify(error);
} catch (e) {
message = `${error} (stringifyError: ${e})`;
}
else message = `${error}`;
message = `${message}`;
return {
message,
stack
};
}
//#endregion
//#region src/shared/utils/compileJsonSchema.ts
const options = {
allErrors: true,
verbose: true,
strict: "log"
};
const ajv$1 = {
draft04: (0, ajv_formats.default)(new ajv_draft_04.default(options)),
draft07: (0, ajv_formats.default)(new ajv.default(options)),
"draft2019-09": (0, ajv_formats.default)(new ajv_dist_2019.default(options)),
"draft2020-12": (0, ajv_formats.default)(new ajv_dist_2020.default(options))
};
function compileJsonSchema(schema) {
const schemaVersion = extractSchemaVersion(schema);
if (schemaVersion.includes("draft-04")) return ajv$1.draft04.compile(schema);
if (schemaVersion.includes("draft-07")) return ajv$1.draft07.compile(schema);
if (schemaVersion.includes("draft/2019-09")) return ajv$1["draft2019-09"].compile(schema);
if (schemaVersion.includes("draft/2020-12")) return ajv$1["draft2020-12"].compile(schema);
throw new Error(`Unsupported JSON Schema version "${schemaVersion}"`);
}
function formatJsonSchemaErrors(schema, errors) {
const schemaVersion = extractSchemaVersion(schema);
if (schemaVersion.includes("draft-04")) return ajv$1.draft04.errorsText(errors).replace(/^data\//gm, "");
if (schemaVersion.includes("draft-07")) return ajv$1.draft07.errorsText(errors).replace(/^data\//gm, "");
if (schemaVersion.includes("draft/2019-09")) return ajv$1["draft2019-09"].errorsText(errors).replace(/^data\//gm, "");
if (schemaVersion.includes("draft/2020-12")) return ajv$1["draft2020-12"].errorsText(errors).replace(/^data\//gm, "");
throw new Error(`Unsupported JSON Schema version "${schemaVersion}"`);
}
function extractSchemaVersion(schema) {
const defaultVersion = "https://json-schema.org/draft-04/schema";
const extractedVersion = schema.$schema;
return typeof extractedVersion === "string" ? extractedVersion : defaultVersion;
}
//#endregion
//#region src/shared/utils/isBrowser.ts
function isBrowser() {
return typeof globalThis.window !== "undefined";
}
function isWorker() {
return typeof globalThis.WorkerGlobalScope !== "undefined" && self instanceof globalThis.WorkerGlobalScope;
}
//#endregion
//#region src/shared/replicants.shared.ts
/**
* If you're wondering why some things are prefixed with "_",
* but not marked as protected or private, this is because our Proxy
* trap handlers need to access these parts of the Replicant internals,
* but don't have access to private or protected members.
*
* So, we code this like its 2010 and just use "_" on some public members.
*/
var AbstractReplicant = class extends require_typed_emitter.TypedEmitter {
name;
namespace;
opts;
revision = 0;
log;
schema;
schemaSum;
status = "undeclared";
validationErrors = [];
_value;
_oldValue;
_operationQueue = [];
_pendingOperationFlush = false;
constructor(name, namespace, opts = {}) {
super();
if (!name || typeof name !== "string") throw new Error("Must supply a name when instantiating a Replicant");
if (!namespace || typeof namespace !== "string") throw new Error("Must supply a namespace when instantiating a Replicant");
if (typeof opts.persistent === "undefined") opts.persistent = true;
if (typeof opts.persistenceInterval === "undefined") opts.persistenceInterval = DEFAULT_PERSISTENCE_INTERVAL;
this.name = name;
this.namespace = namespace;
this.opts = opts;
const originalOnce = this.once.bind(this);
this.once = (event, listener) => {
if (event === "change" && this.status === "declared") {
listener(this.value);
return;
}
return originalOnce(event, listener);
};
/**
* When a new "change" listener is added, chances are that the developer wants it to be initialized ASAP.
* However, if this replicant has already been declared previously in this context, their "change"
* handler will *not* get run until another change comes in, which may never happen for Replicants
* that change very infrequently.
* To resolve this, we immediately invoke all new "change" handlers if appropriate.
*/
this.on("newListener", (event, listener) => {
if (event === "change" && this.status === "declared") listener(this.value);
});
}
/**
* If the operation is an array mutator method, call it on the target array with the operation arguments.
* Else, handle it with objectPath.
*/
_applyOperation(operation) {
ignoreProxy(this);
let result;
const path = pathStrToPathArr(operation.path);
if (ARRAY_MUTATOR_METHODS.includes(operation.method)) {
if (typeof this.value !== "object" || this.value === null) throw new Error(`expected replicant "${this.namespace}:${this.name}" to have a value with type "object", got "${typeof this.value}" instead`);
const arr = object_path.default.get(this.value, path);
if (!Array.isArray(arr)) throw new Error(`expected to find an array in replicant "${this.namespace}:${this.name}" at path "${operation.path}"`);
result = arr[operation.method].apply(arr, "args" in operation && "mutatorArgs" in operation.args ? operation.args.mutatorArgs : []);
proxyRecursive(this, arr, operation.path);
} else switch (operation.method) {
case "overwrite": {
const { newValue } = operation.args;
this[isBrowser() || isWorker() ? "value" : "_value"] = proxyRecursive(this, newValue, operation.path);
result = true;
break;
}
case "add":
case "update": {
path.push(operation.args.prop);
let { newValue } = operation.args;
if (typeof newValue === "object") newValue = proxyRecursive(this, newValue, pathArrToPathStr(path));
result = object_path.default.set(this.value, path, newValue);
break;
}
case "delete":
if (path.length === 0 || object_path.default.has(this.value, path)) {
const target = object_path.default.get(this.value, path);
result = delete target[operation.args.prop];
}
break;
default:
/* istanbul ignore next */
throw new Error(`Unexpected operation method "${operation.method}"`);
}
resumeProxy(this);
return result;
}
/**
* Used to validate the new value of a replicant.
*
* This is a stub that will be replaced if a Schema is available.
*/
validate = () => true;
/**
* Generates a JSON Schema validator function from the `schema` property of the provided replicant.
* @param replicant {object} - The Replicant to perform the operation on.
* @returns {function} - The generated validator function.
*/
_generateValidator() {
const { schema } = this;
if (!schema) throw new Error("can't generate a validator for a replicant which lacks a schema");
let validate;
try {
validate = compileJsonSchema(schema);
} catch (error) {
throw new Error(`Error compiling JSON Schema for Replicant "${this.namespace}:${this.name}":\n\t${stringifyError(error)}`);
}
/**
* Validates a value against the current Replicant's schema.
* Throws when the value fails validation.
* @param [value=replicant.value] {*} - The value to validate. Defaults to the replicant's current value.
* @param [opts] {Object}
* @param [opts.throwOnInvalid = true] {Boolean} - Whether or not to immediately throw when the provided value fails validation against the schema.
*/
return function(value = this.value, { throwOnInvalid = true } = {}) {
const valid = validate(value);
if (!valid) {
this.validationErrors = validate.errors;
if (throwOnInvalid) throw new Error(`Invalid value rejected for replicant "${this.name}" in namespace "${this.namespace}":\n${formatJsonSchemaErrors(schema, validate.errors)}`);
}
return valid;
};
}
};
const proxyMetadataMap = /* @__PURE__ */ new WeakMap();
const metadataMap = /* @__PURE__ */ new WeakMap();
const proxySet = /* @__PURE__ */ new WeakSet();
const ignoringProxy = /* @__PURE__ */ new WeakSet();
const ARRAY_MUTATOR_METHODS = [
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"shift",
"sort",
"splice",
"unshift"
];
/**
* The default persistence interval, in milliseconds.
*/
const DEFAULT_PERSISTENCE_INTERVAL = 100;
function ignoreProxy(replicant) {
ignoringProxy.add(replicant);
}
function resumeProxy(replicant) {
ignoringProxy.delete(replicant);
}
function isIgnoringProxy(replicant) {
return ignoringProxy.has(replicant);
}
const deleteTrap = function(target, prop) {
const metadata = metadataMap.get(target);
if (!metadata) throw new Error("arrived at delete trap without any metadata");
const { replicant } = metadata;
if (isIgnoringProxy(replicant)) return delete target[prop];
if (!{}.hasOwnProperty.call(target, prop)) return true;
if (replicant.schema) {
const valueClone = (0, klona_json.klona)(replicant.value);
const targetClone = object_path.default.get(valueClone, pathStrToPathArr(metadata.path));
delete targetClone[prop];
replicant.validate(valueClone);
}
replicant._addOperation({
path: metadata.path,
method: "delete",
args: { prop }
});
if (!isBrowser() && !isWorker()) return delete target[prop];
};
const CHILD_ARRAY_HANDLER = {
get(target, prop) {
const metadata = metadataMap.get(target);
if (!metadata) throw new Error("arrived at get trap without any metadata");
const { replicant } = metadata;
if (isIgnoringProxy(replicant)) return target[prop];
if ({}.hasOwnProperty.call(Array.prototype, prop) && typeof Array.prototype[prop] === "function" && target[prop] === Array.prototype[prop] && ARRAY_MUTATOR_METHODS.includes(prop)) return (...args) => {
if (replicant.schema) {
const valueClone = (0, klona_json.klona)(replicant.value);
const targetClone = object_path.default.get(valueClone, pathStrToPathArr(metadata.path));
targetClone[prop].apply(targetClone, args);
replicant.validate(valueClone);
}
if (isBrowser() || isWorker()) metadata.replicant._addOperation({
path: metadata.path,
method: prop,
args: { mutatorArgs: Array.prototype.slice.call(args) }
});
else {
ignoreProxy(replicant);
metadata.replicant._addOperation({
path: metadata.path,
method: prop,
args: { mutatorArgs: Array.prototype.slice.call(args) }
});
const retValue = target[prop].apply(target, args);
resumeProxy(replicant);
proxyRecursive(replicant, target, metadata.path);
return retValue;
}
};
return target[prop];
},
set(target, prop, newValue) {
if (target[prop] === newValue) return true;
const metadata = metadataMap.get(target);
if (!metadata) throw new Error("arrived at set trap without any metadata");
const { replicant } = metadata;
if (isIgnoringProxy(replicant)) {
target[prop] = newValue;
return true;
}
if (replicant.schema) {
const valueClone = (0, klona_json.klona)(replicant.value);
const targetClone = object_path.default.get(valueClone, pathStrToPathArr(metadata.path));
targetClone[prop] = newValue;
replicant.validate(valueClone);
}
if ({}.hasOwnProperty.call(target, prop)) replicant._addOperation({
path: metadata.path,
method: "update",
args: {
prop,
newValue
}
});
else replicant._addOperation({
path: metadata.path,
method: "add",
args: {
prop,
newValue
}
});
if (!isBrowser() && !isWorker()) target[prop] = proxyRecursive(metadata.replicant, newValue, joinPathParts(metadata.path, prop));
return true;
},
deleteProperty: deleteTrap
};
const CHILD_OBJECT_HANDLER = {
get(target, prop) {
const value = target[prop];
const tag = Object.prototype.toString.call(value);
if (prop !== "constructor" && (tag === "[object Function]" || tag === "[object AsyncFunction]" || tag === "[object GeneratorFunction]")) return value.bind(target);
return value;
},
set(target, prop, newValue) {
if (target[prop] === newValue) return true;
const metadata = metadataMap.get(target);
if (!metadata) throw new Error("arrived at set trap without any metadata");
const { replicant } = metadata;
if (isIgnoringProxy(replicant)) {
target[prop] = newValue;
return true;
}
if (replicant.schema) {
const valueClone = (0, klona_json.klona)(replicant.value);
const targetClone = object_path.default.get(valueClone, pathStrToPathArr(metadata.path));
targetClone[prop] = newValue;
replicant.validate(valueClone);
}
if ({}.hasOwnProperty.call(target, prop)) replicant._addOperation({
path: metadata.path,
method: "update",
args: {
prop,
newValue
}
});
else replicant._addOperation({
path: metadata.path,
method: "add",
args: {
prop,
newValue
}
});
if (!isBrowser() && !isWorker()) target[prop] = proxyRecursive(metadata.replicant, newValue, joinPathParts(metadata.path, prop));
return true;
},
deleteProperty: deleteTrap
};
/**
* Recursively Proxies an Array or Object. Does nothing to primitive values.
* @param replicant {object} - The Replicant in which to do the work.
* @param value {*} - The value to recursively Proxy.
* @param path {string} - The objectPath to this value.
* @returns {*} - The recursively Proxied value (or just `value` unchanged, if `value` is a primitive)
* @private
*/
function proxyRecursive(replicant, value, path) {
if (typeof value === "object" && value !== null) {
let p;
assertSingleOwner(replicant, value);
if (proxySet.has(value)) {
p = value;
const metadata = proxyMetadataMap.get(value);
metadata.path = path;
} else if (metadataMap.has(value)) {
const metadata = metadataMap.get(value);
if (!metadata) throw new Error("metadata unexpectedly not found");
p = metadata.proxy;
metadata.path = path;
} else {
const handler = Array.isArray(value) ? CHILD_ARRAY_HANDLER : CHILD_OBJECT_HANDLER;
p = new Proxy(value, handler);
proxySet.add(p);
const metadata = {
replicant,
path,
proxy: p
};
metadataMap.set(value, metadata);
proxyMetadataMap.set(p, metadata);
}
for (const key in value) {
/* istanbul ignore if */
if (!{}.hasOwnProperty.call(value, key)) continue;
const escapedKey = key.replace(/\//g, "~1");
if (path) {
const joinedPath = joinPathParts(path, escapedKey);
value[key] = proxyRecursive(replicant, value[key], joinedPath);
} else value[key] = proxyRecursive(replicant, value[key], escapedKey);
}
return p;
}
return value;
}
function joinPathParts(part1, part2) {
return part1.endsWith("/") ? `${part1}${part2}` : `${part1}/${part2}`;
}
/**
* Converts a string path (/a/b/c) to an array path ['a', 'b', 'c']
* @param path {String} - The path to convert.
* @returns {Array} - The converted path.
*/
function pathStrToPathArr(path) {
const pathArr = path.substr(1).split("/").map((part) => part.replace(/~1/g, "/"));
if (pathArr.length === 1 && pathArr[0] === "") return [];
return pathArr;
}
/**
* Converts an array path ['a', 'b', 'c'] to a string path /a/b/c)
* @param path {Array} - The path to convert.
* @returns {String} - The converted path.
*/
function pathArrToPathStr(path) {
const strPath = path.join("/");
if (!strPath.startsWith("/")) return `/${strPath}`;
return strPath;
}
/**
* Throws an exception if an object belongs to more than one Replicant.
* @param replicant {object} - The Replicant that this value should belong to.
* @param value {*} - The value to check ownership of.
*/
function assertSingleOwner(replicant, value) {
let metadata;
if (proxySet.has(value)) metadata = proxyMetadataMap.get(value);
else if (metadataMap.has(value)) metadata = metadataMap.get(value);
else return;
if (metadata.replicant !== replicant) throw new Error(`This object belongs to another Replicant, ${metadata.replicant.namespace}::${metadata.replicant.name}.\nA given object cannot belong to multiple Replicants. Object value:\n${JSON.stringify(value, null, 2)}`);
}
//#endregion
Object.defineProperty(exports, 'ARRAY_MUTATOR_METHODS', {
enumerable: true,
get: function () {
return ARRAY_MUTATOR_METHODS;
}
});
Object.defineProperty(exports, 'AbstractReplicant', {
enumerable: true,
get: function () {
return AbstractReplicant;
}
});
Object.defineProperty(exports, 'ignoreProxy', {
enumerable: true,
get: function () {
return ignoreProxy;
}
});
Object.defineProperty(exports, 'isIgnoringProxy', {
enumerable: true,
get: function () {
return isIgnoringProxy;
}
});
Object.defineProperty(exports, 'proxyRecursive', {
enumerable: true,
get: function () {
return proxyRecursive;
}
});
Object.defineProperty(exports, 'resumeProxy', {
enumerable: true,
get: function () {
return resumeProxy;
}
});
//# sourceMappingURL=replicants.shared.js.map