UNPKG

@nodecg/types

Version:

Dynamic broadcast graphics rendered in a browser

515 lines (510 loc) 18.4 kB
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