UNPKG

typeson

Version:

Preserves types over JSON, BSON or socket.io

1,505 lines (1,416 loc) 64.8 kB
'use strict'; function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), Object.defineProperty(e, "prototype", { writable: false }), e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = true, o = false; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = true, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return (String )(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } /* eslint-disable promise/avoid-new -- Own API */ /** * We keep this function minimized so if using two instances of this * library, where one is minimized and one is not, it will still work * with `hasConstructorOf`. * With ES6 classes, we may be able to simply use `class TypesonPromise * extends Promise` and add a string tag for detection. * @template T */ var TypesonPromise = /*#__PURE__*/_createClass( /** * @param {( * resolve: (value: any) => any, * reject: (reason?: any) => void * ) => void} f */ function TypesonPromise(f) { _classCallCheck(this, TypesonPromise); this.p = new Promise(f); }); /* eslint-enable promise/avoid-new -- Own API */ // eslint-disable-next-line @stylistic/max-len -- Long // class TypesonPromise extends Promise {get[Symbol.toStringTag](){return 'TypesonPromise'};} // eslint-disable-line keyword-spacing, space-before-function-paren, space-before-blocks, block-spacing, semi // eslint-disable-next-line camelcase -- Special identifier TypesonPromise.__typeson__type__ = 'TypesonPromise'; // Note: core-js-bundle provides a `Symbol` polyfill /* istanbul ignore else */ if (typeof Symbol !== 'undefined') { // Ensure `isUserObject` will return `false` for `TypesonPromise` Object.defineProperty(TypesonPromise.prototype, Symbol.toStringTag, { get: function get() { return 'TypesonPromise'; } }); } /* eslint-disable unicorn/no-thenable -- Desired to be Promise-like */ /** * * @param {?(value: T) => any} [onFulfilled] * @param {(reason?: any) => any} [onRejected] * @returns {TypesonPromise<T>} */ TypesonPromise.prototype.then = function (onFulfilled, onRejected) { var _this = this; return new TypesonPromise(function (typesonResolve, typesonReject) { // eslint-disable-next-line @stylistic/max-len -- Long // eslint-disable-next-line promise/catch-or-return -- Handling ourselves _this.p.then(function (res) { // eslint-disable-next-line @stylistic/max-len -- Long // eslint-disable-next-line promise/always-return -- Handle ourselves typesonResolve(onFulfilled ? onFulfilled(res) : res); })["catch"](function (/** @type {unknown} */res) { return onRejected ? onRejected(res) // eslint-disable-next-line @stylistic/max-len -- Long // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- Is an error : Promise.reject(res); }).then(typesonResolve, typesonReject); }); }; /** * * @param {(reason?: any) => void} onRejected * @returns {TypesonPromise<T>} */ TypesonPromise.prototype["catch"] = function (onRejected) { return this.then(function () { return undefined; }, onRejected); }; /** * @template T * @param {T} v * @returns {TypesonPromise<T>} */ TypesonPromise.resolve = function (v) { return new TypesonPromise(function (typesonResolve) { typesonResolve(v); }); }; /** * @template T * @param {any} v * @returns {TypesonPromise<T>} */ TypesonPromise.reject = function (v) { return new TypesonPromise(function (typesonResolve, typesonReject) { typesonReject(v); }); }; /** * * @template T * @param {(TypesonPromise<T>|Promise<T>|any)[]} promArr * @returns {TypesonPromise<T>} */ TypesonPromise.all = function (promArr) { return new TypesonPromise(function (typesonResolve, typesonReject) { // eslint-disable-next-line promise/catch-or-return -- Handle ourselves Promise.all(promArr.map(function (prom) { return prom !== null && prom !== void 0 && prom.constructor && '__typeson__type__' in prom.constructor && prom.constructor.__typeson__type__ === 'TypesonPromise' ? /** @type {TypesonPromise<T>} */prom.p : prom; })).then(typesonResolve, typesonReject); }); }; /** * @template T * @param {(TypesonPromise<T>|Promise<T>|null)[]} promArr * @returns {TypesonPromise<T>} */ TypesonPromise.race = function (promArr) { return new TypesonPromise(function (typesonResolve, typesonReject) { // eslint-disable-next-line promise/catch-or-return -- Handle ourselves Promise.race(promArr.map(function (prom) { return prom !== null && prom !== void 0 && prom.constructor && '__typeson__type__' in prom.constructor && prom.constructor.__typeson__type__ === 'TypesonPromise' ? /** @type {TypesonPromise<T>} */prom.p : prom; })).then(typesonResolve, typesonReject); }); }; /** * @template T * @param {(TypesonPromise<T>|Promise<T>|null)[]} promArr * @returns {TypesonPromise<T>} */ TypesonPromise.allSettled = function (promArr) { return new TypesonPromise(function (typesonResolve, typesonReject) { // eslint-disable-next-line promise/catch-or-return -- Handle ourselves Promise.allSettled(promArr.map(function (prom) { return prom !== null && prom !== void 0 && prom.constructor && '__typeson__type__' in prom.constructor && prom.constructor.__typeson__type__ === 'TypesonPromise' ? /** @type {TypesonPromise<T>} */prom.p : prom; })).then(typesonResolve, typesonReject); }); }; var hasOwn$1 = Object.hasOwn, getProto = Object.getPrototypeOf; /** * Second argument not in use internally, but provided for utility. * @param {any} v * @param {boolean} [catchCheck] * @returns {boolean} */ function isThenable(v, catchCheck) { return isObject(v) && typeof v.then === 'function' && (!catchCheck || typeof v["catch"] === 'function'); } /** * * @param {any} val * @returns {string} */ function toStringTag(val) { return Object.prototype.toString.call(val).slice(8, -1); } /** * This function is dependent on both constructors * being identical so any minimization is expected of both. * @param {any} a * @param {({__typeson__type__?: string} & Function)|null} b * @returns {boolean} */ function hasConstructorOf(a, b) { if (!a || _typeof(a) !== 'object') { return false; } var proto = getProto(a); if (!proto) { return b === null; } var Ctor = hasOwn$1(proto, 'constructor') && proto.constructor; if (typeof Ctor !== 'function') { return b === null; } if (b === Ctor) { return true; } if (b !== null && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(b)) { return true; } // eslint-disable-next-line sonarjs/prefer-single-boolean-return -- Cleaner if (typeof b === 'function' && typeof Ctor.__typeson__type__ === 'string' && Ctor.__typeson__type__ === b.__typeson__type__) { return true; } return false; } /** * * @param {any} val * @returns {boolean} */ function isPlainObject(val) { // Mirrors jQuery's if (!val || toStringTag(val) !== 'Object') { return false; } var proto = getProto(val); if (!proto) { // `Object.create(null)` return true; } return hasConstructorOf(val, Object); } /** * * @param {any} val * @returns {boolean} */ function isUserObject(val) { if (!val || toStringTag(val) !== 'Object') { return false; } var proto = getProto(val); if (!proto) { // `Object.create(null)` return true; } return hasConstructorOf(val, Object) || isUserObject(proto); } /** * * @param {any} v * @returns {boolean} */ function isObject(v) { return v !== null && _typeof(v) === 'object'; } /** * * @param {string} keyPathComponent * @returns {string} */ function escapeKeyPathComponent(keyPathComponent) { return keyPathComponent.replaceAll("''", "''''").replace(/^$/, "''").replaceAll('~', '~0').replaceAll('.', '~1'); } /** * * @param {string} keyPathComponent * @returns {string} */ function unescapeKeyPathComponent(keyPathComponent) { return keyPathComponent.replaceAll('~1', '.').replaceAll('~0', '~').replace(/^''$/, '').replaceAll("''''", "''"); } /** * @typedef {null|boolean|number|string} Primitive */ /** * @typedef {Primitive|Primitive[]|{[key: string]: JSON}} JSON */ /** * @param {any} obj * @param {string} keyPath * @throws {TypeError} * @returns {any} */ function getByKeyPath(obj, keyPath) { if (keyPath === '') { return obj; } if (obj === null || _typeof(obj) !== 'object') { throw new TypeError('Unexpected non-object type'); } var period = keyPath.indexOf('.'); if (period !== -1) { var innerObj = /** @type {{[key: string]: any|undefined}} */obj[unescapeKeyPathComponent(keyPath.slice(0, period))]; return innerObj === undefined ? undefined : getByKeyPath(innerObj, keyPath.slice(period + 1)); } return /** @type {{[key: string]: any}} */obj[unescapeKeyPathComponent(keyPath)]; } /** * @typedef {{ * [key: string]: NestedObject|any * }} NestedObject */ /** * * @param {unknown} obj * @param {string} keyPath * @param {any} value * @throws {TypeError} * @returns {any} */ function setAtKeyPath(obj, keyPath, value) { if (keyPath === '') { return value; } // We allow arrays, however if (!obj || _typeof(obj) !== 'object') { throw new TypeError('Unexpected non-object type'); } if (keyPath === '__proto__') { throw new TypeError('Invalid property'); } var period = keyPath.indexOf('.'); if (period !== -1) { var innerObj = /** @type {{[key: string]: any}} */obj[unescapeKeyPathComponent(keyPath.slice(0, period))]; return setAtKeyPath(innerObj, keyPath.slice(period + 1), value); } /** @type {{[key: string]: any}} */ obj[unescapeKeyPathComponent(keyPath)] = value; return obj; } /** * @typedef {"null"|"array"|"undefined"|"boolean"|"number"|"string"| * "object"|"symbol"|"bigint"|"function"} ObjectTypeString */ /** * * @param {any} value * @returns {ObjectTypeString} */ function getJSONType(value) { return value === null ? 'null' : Array.isArray(value) ? 'array' : _typeof(value); } function _await(value, then, direct) { if (!value || !value.then) { value = Promise.resolve(value); } return then ? value.then(then) : value; } var keys = Object.keys, hasOwn = Object.hasOwn, isArray = Array.isArray, internalStateObjPropsToIgnore = ['type', 'replaced', 'iterateIn', 'iterateUnsetNumeric', 'addLength']; /** * @typedef {object} PlainObjectType * @property {string} keypath * @property {string} type */ /** * Handle plain object revivers first so reference setting can use * revived type (e.g., array instead of object); assumes revived * has same structure or will otherwise break subsequent references. * @param {PlainObjectType} a * @param {PlainObjectType} b * @returns {1|-1|0} */ function _async(f) { return function () { for (var args = [], i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } try { return Promise.resolve(f.apply(this, args)); } catch (e) { return Promise.reject(e); } }; } /** * @typedef {object} KeyPathEvent * @property {string} [cyclicKeypath] */ /** * @typedef {object} EndIterateInEvent * @property {boolean} [endIterateIn] * @property {boolean} [end] */ /** * @typedef {{ * endIterateOwn?: boolean * }} EndIterateOwnEvent */ /** * @typedef {object} EndIterateUnsetNumericEvent * @property {boolean} [endIterateUnsetNumeric] * @property {boolean} [end] */ /** * @typedef {object} TypeDetectedEvent * @property {boolean} [typeDetected] */ /** * @typedef {object} ReplacingEvent * @property {boolean} [replacing] */ /** * @typedef {[ * keyPath: string, * value: object|Array<any>|TypesonPromise<any>, * cyclic: boolean|"readonly"|undefined, * stateObj: StateObject, * clone: {[key: (string|Integer)]: any}|undefined, * key: string|Integer|undefined, * stateObjType: string|undefined * ][]} PromisesData */ /** * @typedef {KeyPathEvent & EndIterateInEvent & EndIterateOwnEvent & * EndIterateUnsetNumericEvent & * TypeDetectedEvent & ReplacingEvent & {} & { * replaced?: any * } & { * clone?: {[key: string]: any} * } & { * keypath: string, * value: any, * cyclic: boolean|undefined|"readonly", * stateObj: StateObject, * promisesData: PromisesData, * resolvingTypesonPromise: ?boolean|undefined, * awaitingTypesonPromise: boolean * } & {type: string}} ObserverData */ /** * @typedef {(data: ObserverData) => void} EncapsulateObserver */ /** * @callback Observer * @param {KeyPathEvent|EndIterateInEvent|EndIterateOwnEvent| * EndIterateUnsetNumericEvent| * TypeDetectedEvent|ReplacingEvent} [event] * @returns {void} */ /** * @typedef {object} TypesonOptions * @property {boolean} [stringification] Auto-set by `stringify` * @property {boolean} [parse] Auto-set by `parse` * @property {boolean} [sync] Can be overridden when auto-set by * `encapsulate` and `revive`. * @property {boolean} [returnTypeNames] Auto-set by `specialTypeNames` * @property {boolean} [iterateNone] Auto-set by `rootTypeName` * @property {boolean} [cyclic] * @property {boolean} [throwOnBadSyncType] Auto-set by `stringifyAsync`, * `stringifySync`, `parseSync`, `parseAsync`, `encapsulateSync`, * `encapsulateAync`, `reviveSync`, `reviveAsync` * @property {number|boolean} [fallback] `true` sets to 0. Default is * positive infinity. Used within `register` * @property {EncapsulateObserver} [encapsulateObserver] */ /** * An instance of this class can be used to call `stringify()` and `parse()`. * Typeson resolves cyclic references by default. Can also be extended to * support custom types using the register() method. * * @class * @param {{cyclic: boolean}} [options] - if cyclic (default true), * cyclic references will be handled gracefully. */ function _invoke(body, then) { var result = body(); if (result && result.then) { return result.then(then); } return then(result); } function nestedPathsFirst(a, b) { var _a$keypath$match, _b$keypath$match; if (a.keypath === '') { return -1; } var as = (_a$keypath$match = a.keypath.match(/\./g)) !== null && _a$keypath$match !== void 0 ? _a$keypath$match : 0; var bs = (_b$keypath$match = b.keypath.match(/\./g)) !== null && _b$keypath$match !== void 0 ? _b$keypath$match : 0; if (as) { as = /** @type {RegExpMatchArray} */as.length; } if (bs) { bs = /** @type {RegExpMatchArray} */bs.length; } return as > bs ? -1 : as < bs ? 1 : a.keypath < b.keypath ? -1 : a.keypath > b.keypath ? 1 // Keypath should never be the same /* c8 ignore next 1 */ : 0; } var Typeson = /*#__PURE__*/function () { /** * @param {TypesonOptions} [options] */ function Typeson(options) { _classCallCheck(this, Typeson); this.options = options; // Replacers signature: replace (value). Returns falsy if not // replacing. Otherwise ['Date', value.getTime()] /** @type {ReplacerObject[]} */ this.plainObjectReplacers = []; /** @type {ReplacerObject[]} */ this.nonplainObjectReplacers = []; // Revivers: [{type => reviver}, {plain: boolean}]. // Sample: [{'Date': value => new Date(value)}, {plain: false}] /** * @type {{ * [key: string]: [ * ReviverObject|undefined, * {plain: boolean|undefined} * ]|undefined}} */ this.revivers = {}; /** Types registered via `register()`. */ /** @type {TypeSpecSet} */ this.types = {}; } /** * @typedef {null|boolean|number|string} Primitive */ /** * @typedef {Primitive|Primitive[]|{[key: string]: JSON}} JSON */ /** * @callback JSONReplacer * @param {""|string} key * @param {JSON} value * @returns {any} * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20replacer%20parameter */ /** * Serialize given object to Typeson. * Initial arguments work identical to those of `JSON.stringify`. * The `replacer` argument has nothing to do with our replacers. * @param {any} obj * @param {?(JSONReplacer|string[]|undefined)} [replacer] * @param {number|string|null|undefined} [space] * @param {TypesonOptions} [opts] * @returns {string|Promise<string>} Promise resolves to a string */ return _createClass(Typeson, [{ key: "stringify", value: function stringify(obj, replacer, space, opts) { opts = _objectSpread2(_objectSpread2(_objectSpread2({}, this.options), opts), {}, { stringification: true }); var encapsulated = this.encapsulate(obj, null, opts); if (isArray(encapsulated)) { return JSON.stringify(encapsulated[0], // Casting type due to error in JSON.stringify type // not accepting `null` /** @type {JSONReplacer|undefined} */ replacer, /** @type {string|number|undefined} */space); } return /** @type {Promise<string>} */encapsulated.then(function (res) { return JSON.stringify(res, // Casting type due to error in JSON.stringify type /** @type {JSONReplacer|undefined} */ replacer, /** @type {string|number|undefined} */space); }); } /** * Also sync but throws on non-sync result. * @param {any} obj * @param {?(JSONReplacer|string[]|undefined)} [replacer] * @param {number|string} [space] * @param {TypesonOptions} [opts] * @returns {string} */ }, { key: "stringifySync", value: function stringifySync(obj, replacer, space, opts) { return /** @type {string} */this.stringify(obj, replacer, space, _objectSpread2(_objectSpread2({ throwOnBadSyncType: true }, opts), {}, { sync: true })); } /** * * @param {any} obj * @param {JSONReplacer|string[]|null|undefined} [replacer] * @param {number|string|null|undefined} [space] * @param {TypesonOptions} [opts] * @returns {Promise<string>} */ }, { key: "stringifyAsync", value: function stringifyAsync(obj, replacer, space, opts) { return /** @type {Promise<string>} */this.stringify(obj, replacer, space, _objectSpread2(_objectSpread2({ throwOnBadSyncType: true }, opts), {}, { sync: false })); } /** * @callback JSONReviver * @param {string} key * @param {JSON} value * @returns {JSON} */ /** * Parse Typeson back into an obejct. * Initial arguments works identical to those of `JSON.parse()`. * @param {string} text * @param {?JSONReviver} [reviver] This JSON reviver has nothing to do with * our revivers. * @param {TypesonOptions} [opts] * @returns {any|Promise<any>} */ }, { key: "parse", value: function parse(text, reviver, opts) { opts = _objectSpread2(_objectSpread2(_objectSpread2({}, this.options), opts), {}, { parse: true }); return this.revive(JSON.parse(text, /** @type {JSONReviver|undefined} */reviver), opts); } /** * Also sync but throws on non-sync result. * @param {string} text * @param {JSONReviver} [reviver] This JSON reviver has nothing to do with * our revivers. * @param {TypesonOptions} [opts] * @returns {any} */ }, { key: "parseSync", value: function parseSync(text, reviver, opts) { return this.parse(text, reviver, _objectSpread2(_objectSpread2({ throwOnBadSyncType: true }, opts), {}, { sync: true })); } /** * @param {string} text * @param {JSONReviver} [reviver] This JSON reviver has nothing to do with * our revivers. * @param {TypesonOptions} [opts] * @returns {Promise<any>} */ }, { key: "parseAsync", value: function parseAsync(text, reviver, opts) { return this.parse(text, reviver, _objectSpread2(_objectSpread2({ throwOnBadSyncType: true }, opts), {}, { sync: false })); } /** * * @param {any} obj * @param {StateObject|null|undefined} [stateObj] * @param {TypesonOptions} [opts] * @returns {string[]|false} */ }, { key: "specialTypeNames", value: function specialTypeNames(obj, stateObj) { var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; opts.returnTypeNames = true; return /** @type {string[]|false} */this.encapsulate(obj, stateObj, opts); } /** * * @param {any} obj * @param {StateObject|null|undefined} [stateObj] * @param {TypesonOptions} [opts] * @returns {Promise<ObjectTypeString|string>|ObjectTypeString|string} */ }, { key: "rootTypeName", value: function rootTypeName(obj, stateObj) { var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; opts.iterateNone = true; return ( /** * @type {Promise<ObjectTypeString|string>| * ObjectTypeString|string} */ this.encapsulate(obj, stateObj, opts) ); } /** * Encapsulate a complex object into a plain Object by replacing * registered types with plain objects representing the types data. * * This method is used internally by `Typeson.stringify()`. * @param {any} obj - Object to encapsulate. * @param {StateObject|null|undefined} [stateObj] * @param {TypesonOptions} [options] * @returns {Promise<any>| * {[key: (string|Integer)]: any}|any| * ObjectTypeString|string|string[]| * false * } The ObjectTypeString, string and string[] should only be returned * for `specialTypeNames` and `rootTypeName` calls, not direct use of * this function. */ }, { key: "encapsulate", value: function encapsulate(obj, stateObj, options) { var _this = this; var opts = _objectSpread2(_objectSpread2({ sync: true }, this.options), options); var sync = opts.sync; /** * @type {{ * [key: string]: '#'|string|string[] * }} */ var types = {}, /** @type {object[]} */ refObjs = [], // For checking cyclic references /** @type {string[]} */ refKeys = [], // For checking cyclic references /** @type {PromisesData} */ promisesDataRoot = []; // Clone the object deeply while at the same time replacing any // special types or cyclic reference: var cyclic = 'cyclic' in opts ? opts.cyclic : true; var encapsulateObserver = opts.encapsulateObserver; /** * * @param {any} _ret * @returns {{[key: (string|Integer)]: any}|string|string[]| * ObjectTypeString|any|false} */ var finish = function finish(_ret) { // Add `$types` to result only if we ever bumped into a // special type (or special case where object has own `$types`) var typeNames = Object.values(types); if (opts.iterateNone) { if (typeNames.length) { return typeNames[0]; } return getJSONType(_ret); } if (typeNames.length) { if (opts.returnTypeNames) { return _toConsumableArray(new Set(typeNames)); } // Special if array (or a primitive) was serialized // because JSON would ignore custom `$types` prop on it if (!_ret || !isPlainObject(_ret) || // Also need to handle if this is an object with its // own `$types` property (to avoid ambiguity) hasOwn(_ret, '$types')) { _ret = { $: _ret, $types: { $: types } }; } else { _ret.$types = types; } // No special types } else if (isObject(_ret) && hasOwn(_ret, '$types')) { _ret = { $: _ret, $types: true }; } if (opts.returnTypeNames) { return false; } return _ret; }; /** * * @param {any} _ret * @param {PromisesData} promisesData * @returns {Promise<any>} */ var checkPromises = _async(function (_ret, promisesData) { return _await(Promise.all(promisesData.map(function (pd) { return /** @type {TypesonPromise<any>} */pd[1].p; })), function (promResults) { return _await(Promise.all(promResults.map(_async(function (promResult) { var _exit = false; /** @type {PromisesData} */ var newPromisesData = []; var _promisesData$splice = promisesData.splice(0, 1), _promisesData$splice2 = _slicedToArray(_promisesData$splice, 1), prData = _promisesData$splice2[0]; var _prData = _slicedToArray(prData, 7), keyPath = _prData[0], _cyclic = _prData[2], _stateObj = _prData[3], parentObj = _prData[4], key = _prData[5], detectedType = _prData[6]; var encaps = _encapsulate2(keyPath, promResult, _cyclic, _stateObj, newPromisesData, true, detectedType); var isTypesonPromise = hasConstructorOf(encaps, TypesonPromise); // Handle case where an embedded custom type itself // returns a `TypesonPromise` return _invoke(function () { if (keyPath && isTypesonPromise) { return _await(encaps.p, function (encaps2) { // Undefined parent only for root which has no `keyPath` // eslint-disable-next-line @stylistic/max-len -- Long /** @type {{[key: (string|number)]: any}} */ parentObj[(/** @type {string|number} */key)] = encaps2; var _checkPromises = checkPromises(_ret, newPromisesData); _exit = true; return _checkPromises; }); } }, function (_result) { if (_exit) return _result; if (keyPath) { // Undefined parent only for root which has no `keyPath` // eslint-disable-next-line @stylistic/max-len -- Long /** @type {{[key: (string|number)]: any}} */ parentObj[(/** @type {string|number} */key)] = encaps; } else if (isTypesonPromise) { _ret = encaps.p; } else { // If this is itself a `TypesonPromise` (because the // original value supplied was a `Promise` or // because the supplied custom type value resolved // to one), returning it below will be fine since // a `Promise` is expected anyways given current // config (and if not a `Promise`, it will be ready // as the resolve value) _ret = encaps; } return checkPromises(_ret, newPromisesData); }); }))), function () { return _ret; }); }); }); /** * @typedef {object} OwnKeysObject * @property {boolean} ownKeys */ /** * @callback BuiltinStateObjectPropertiesCallback * @returns {void} */ /** * * @param {StateObject} _stateObj * @param {OwnKeysObject} ownKeysObj * @param {BuiltinStateObjectPropertiesCallback} cb * @returns {void} */ var _adaptBuiltinStateObjectProperties = function _adaptBuiltinStateObjectProperties(_stateObj, ownKeysObj, cb) { Object.assign(_stateObj, ownKeysObj); // eslint-disable-next-line @stylistic/max-len -- Long // eslint-disable-next-line sonarjs/function-return-type -- Convenient as is var vals = internalStateObjPropsToIgnore.map(function (prop) { var tmp = _stateObj[prop]; delete _stateObj[prop]; return tmp; }); cb(); internalStateObjPropsToIgnore.forEach(function (prop, i) { // We're just copying from one StateObject to another, // so force TS with a type each can take _stateObj[prop] = /** @type {any} */vals[i]; }); }; /** * * @param {string} keypath * @param {any} value * @param {boolean|undefined|"readonly"} _cyclic * @param {StateObject} _stateObj * @param {PromisesData} promisesData * @param {?boolean} [resolvingTypesonPromise] * @param {string} [detectedType] * @returns {any} */ var _encapsulate2 = function _encapsulate(keypath, value, _cyclic, _stateObj, promisesData, resolvingTypesonPromise, detectedType) { var _ret; /** * @type {{}|{ * replaced: any * }|{ * clone: {[key: string]: any} * }} */ var observerData = {}; var $typeof = _typeof(value); var runObserver = encapsulateObserver // eslint-disable-next-line @stylistic/max-len -- Long // eslint-disable-next-line @stylistic/operator-linebreak -- Needs JSDoc ? // Bug with TS apparently as can't just use // `@type {Observer}` here as doesn't see param is optional /** * @param {KeyPathEvent|EndIterateInEvent|EndIterateOwnEvent| * EndIterateUnsetNumericEvent| * TypeDetectedEvent|ReplacingEvent} [_obj] * @returns {void} */ function (_obj) { var _ref; var type = (_ref = detectedType !== null && detectedType !== void 0 ? detectedType : _stateObj.type) !== null && _ref !== void 0 ? _ref : getJSONType(value); encapsulateObserver(Object.assign(_obj !== null && _obj !== void 0 ? _obj : observerData, { keypath: keypath, value: value, cyclic: _cyclic, stateObj: _stateObj, promisesData: promisesData, resolvingTypesonPromise: resolvingTypesonPromise, awaitingTypesonPromise: hasConstructorOf(value, TypesonPromise) }, { type: type })); } : null; if (['string', 'boolean', 'number', 'undefined'].includes($typeof)) { if (value === undefined || // Numbers that are not supported in JSON Number.isNaN(value) || value === Number.NEGATIVE_INFINITY || value === Number.POSITIVE_INFINITY || // This can be 0 or -0 value === 0) { _ret = _stateObj.replaced ? value : replace(keypath, value, _stateObj, promisesData, false, resolvingTypesonPromise, runObserver); if (_ret !== value) { observerData = { replaced: _ret }; } } else { _ret = value; } if (runObserver) { runObserver(); } return _ret; } if (value === null) { if (runObserver) { runObserver(); } return value; } if (_cyclic && !_stateObj.iterateIn && !_stateObj.iterateUnsetNumeric && value && _typeof(value) === 'object') { // Options set to detect cyclic references and be able // to rewrite them. var refIndex = refObjs.indexOf(value); if (refIndex === -1) { if (_cyclic === true) { refObjs.push(value); refKeys.push(keypath); } } else { types[keypath] = '#'; if (runObserver) { runObserver({ cyclicKeypath: refKeys[refIndex] }); } return '#' + refKeys[refIndex]; } } var isPlainObj = isPlainObject(value); var isArr = isArray(value); var replaced = // Running replace will cause infinite loop as will test // positive again (isPlainObj || isArr) && (!_this.plainObjectReplacers.length || _stateObj.replaced) || _stateObj.iterateIn // Optimization: if plain object and no plain-object // replacers, don't try finding a replacer ? value : replace(keypath, value, _stateObj, promisesData, isPlainObj || isArr, null, runObserver); /** @type {undefined|Array<any>|{[key: string]: any}} */ var clone; if (replaced !== value) { _ret = replaced; observerData = { replaced: replaced }; } else { // eslint-disable-next-line no-lonely-if -- Clearer if (keypath === '' && hasConstructorOf(value, TypesonPromise)) { promisesData.push([keypath, value, _cyclic, _stateObj, undefined, undefined, _stateObj.type]); _ret = value; } else if (isArr && _stateObj.iterateIn !== 'object' || _stateObj.iterateIn === 'array') { // eslint-disable-next-line unicorn/no-new-array -- Sparse clone = new Array(value.length); observerData = { clone: clone }; } else if (!['function', 'symbol'].includes(_typeof(value)) && !('toJSON' in value) && !hasConstructorOf(value, TypesonPromise) && !hasConstructorOf(value, Promise) && !hasConstructorOf(value, ArrayBuffer) || isPlainObj || _stateObj.iterateIn === 'object') { clone = {}; if (_stateObj.addLength) { clone.length = value.length; } observerData = { clone: clone }; } else { _ret = value; // Only clone vanilla objects and arrays } } if (runObserver) { runObserver(); } if (opts.iterateNone) { return clone !== null && clone !== void 0 ? clone : _ret; } if (!clone) { return _ret; } // Iterate object or array if (_stateObj.iterateIn) { var _loop = function _loop(key) { var ownKeysObj = { ownKeys: hasOwn(value, key) }; _adaptBuiltinStateObjectProperties(_stateObj, ownKeysObj, function () { var kp = keypath + (keypath ? '.' : '') + escapeKeyPathComponent(key); var val = _encapsulate2(kp, value[key], Boolean(_cyclic), _stateObj, promisesData, resolvingTypesonPromise); if (hasConstructorOf(val, TypesonPromise)) { promisesData.push([kp, val, Boolean(_cyclic), _stateObj, clone, key, _stateObj.type]); } else if (val !== undefined) { /** @type {{[key: (string|Integer)]: any}} */clone[key] = val; } }); }; // eslint-disable-next-line @stylistic/max-len -- Long // eslint-disable-next-line guard-for-in -- Guard not wanted here for (var key in value) { _loop(key); } if (runObserver) { runObserver({ endIterateIn: true, end: true }); } } else { // Note: Non-indexes on arrays won't survive stringify so // somewhat wasteful for arrays, but so too is iterating // all numeric indexes on sparse arrays when not wanted // or filtering own keys for positive integers keys(value).forEach(function (key) { var kp = keypath + (keypath ? '.' : '') + escapeKeyPathComponent(key); var ownKeysObj = { ownKeys: true }; _adaptBuiltinStateObjectProperties(_stateObj, ownKeysObj, function () { var val = _encapsulate2(kp, value[key], Boolean(_cyclic), _stateObj, promisesData, resolvingTypesonPromise); if (hasConstructorOf(val, TypesonPromise)) { promisesData.push([kp, val, Boolean(_cyclic), _stateObj, clone, key, _stateObj.type]); } else if (val !== undefined) { /** @type {{[key: string]: any}} */clone[key] = val; } }); }); if (runObserver) { runObserver({ endIterateOwn: true, end: true }); } } // Iterate array for non-own numeric properties (we can't // replace the prior loop though as it iterates non-integer // keys) if (_stateObj.iterateUnsetNumeric) { var vl = value.length; var _loop2 = function _loop2(i) { if (!(i in value)) { // No need to escape numeric var kp = "".concat(keypath).concat(keypath ? '.' : '').concat(String(i)); var ownKeysObj = { ownKeys: false }; _adaptBuiltinStateObjectProperties(_stateObj, ownKeysObj, function () { var val = _encapsulate2(kp, undefined, Boolean(_cyclic), _stateObj, promisesData, resolvingTypesonPromise); if (hasConstructorOf(val, TypesonPromise)) { promisesData.push([kp, val, Boolean(_cyclic), _stateObj, clone, i, _stateObj.type]); } else if (val !== undefined) { /** @type {{[key: Integer]: any}} */ clone[i] = val; } }); } }; for (var i = 0; i < vl; i++) { _loop2(i); } if (runObserver) { runObserver({ endIterateUnsetNumeric: true, end: true }); } } return clone; }; /** * * @param {string} keypath * @param {any} value * @param {StateObject} _stateObj * @param {PromisesData} promisesData * @param {boolean} plainObject * @param {?boolean} [resolvingTypesonPromise] * @param {Observer|null} [runObserver] * @throws {Error} * @returns {any} */ var replace = function replace(keypath, value, _stateObj, promisesData, plainObject, resolvingTypesonPromise, runObserver) { // Encapsulate registered types var replacers = plainObject ? _this.plainObjectReplacers : _this.nonplainObjectReplacers; var i = replacers.length; while (i--) { var replacer = replacers[i]; if (replacer.test(value, _stateObj)) { var type = replacer.type; if (_this.revivers[type]) { // Record the type only if a corresponding reviver // exists. This is to support specs where only // replacement is done. // For example, ensuring deep cloning of the object, // or replacing a type to its equivalent without // the need to revive it. var existing = types[keypath]; // type can comprise an array of types (see test // "should support intermediate types") types[keypath] = existing ? [type].concat(existing) : type; } Object.assign(_stateObj, { type: type, replaced: true }); if ((sync || !replacer.replaceAsync) && !replacer.replace) { if (runObserver) { runObserver({ typeDetected: true }); } return _encapsulate2(keypath, value, cyclic && 'readonly', _stateObj, promisesData, resolvingTypesonPromise, type); } if (runObserver) { runObserver({ replacing: true }); } // Now, also traverse the result in case it contains its // own types to replace var replaced = void 0; if (sync || !replacer.replaceAsync) { // Shouldn't reach here due to above condition /* c8 ignore next 3 */ if (typeof replacer.replace === 'undefined') { throw new TypeError('Missing replacer'); } replaced = replacer.replace(value, _stateObj); } else { replaced = replacer.replaceAsync(value, _stateObj); } return _encapsulate2(keypath, replaced, cyclic && 'readonly', _stateObj, promisesData, resolvingTypesonPromise, type); } } return value; }; var ret = _encapsulate2('', obj, cyclic, stateObj !== null && stateObj !== void 0 ? stateObj : {}, promisesDataRoot); if (promisesDataRoot.length) { return sync && opts.throwOnBadSyncType ? function () { throw new TypeError('Sync method requested but async result obtained'); }() : Promise.resolve(checkPromises(ret, promisesDataRoot)).then(finish); } if (!sync && opts.throwOnBadSyncType) { throw new TypeError('Async method requested but sync result obtained'); } // If this is a synchronous request for stringification, yet // a promise is the result, we don't want to resolve leading // to an async result, so we return an array to avoid // ambiguity if (opts.stringification && sync) { return [finish(ret)]; } if (sync) { return finish(ret); } return Promise.resolve(finish(ret)); } /** * Also sync but throws on non-sync result. * @param {any} obj * @param {StateObject|null|undefined} [stateObj] * @param {TypesonOptions} [opts] * @returns {any} */ }, { key: "encapsulateSync", value: function encapsulateSync(obj, stateObj, opts) { return this.encapsulate(obj, stateObj, _objectSpread2(_objectSpread2({ throwOnBadSyncType: true }, opts), {}, { sync: true })); } /** * @param {any} obj * @param {StateObject|null|undefined} [stateObj] * @param {TypesonOptions} [opts] * @returns {Promise<any>} */ }, { key: "encapsulateAsync", value: function encapsulateAsync(obj, stateObj, opts) { return /** @type {Promise<any>} */this.encapsulate(obj, stateObj, _objectSpread2(_objectSpread2({ throwOnBadSyncType: true }, opts), {}, { sync: false })); } /** * Revive an encapsulated object. * This method is used internally by `Typeson.parse()`. * @param {any} obj - Object to revive. If it has a `$types` member, * the properties that are listed there will be replaced with its true * type instead of just plain objects. * @param {TypesonOptions} [options] * @throws {TypeError} If mismatch between sync/async type and result * @returns {Promise<any>|any} If async, returns a Promise that resolves * to `any`. */ }, { key: "revive", value: function revive(obj, options) { var _this2 = this; var opts = _objectSpread2(_objectSpread2({ sync: true }, this.options), options); var sync = opts.sync; /** * @param {any} val * @throws {TypeError} * @returns {any|Promise<any>} */ function finishRevival(val) { if (sync) { return val; } if (opts.throwOnBadSyncType) { throw new TypeError('Async method requested but sync result obtained'); } return Promise.resolve(val); } if (!obj || _typeof(obj) !== 'object' || Array.isArray(obj)) { return finishRevival(obj); } var types = obj.$types; // Object happened to have own `$types` property but with // no actual types, so we unescape and return that object if (types === true) { return finishRevival(obj.$); } // No type info added. Revival not needed. if (!types || _typeof(types) !== 'object' || Array.isArray(types)) { return finishRevival(obj); } /** * Should be a `clone` and `key` present as pushed when non-root. * @type {[ * target: ?{[key: string]: any}, * keypath: string, * clone: {[key: string]: any}, * key: string * ][]} */ var keyPathResolutions = []; var stateObj = {}; var ignore$Types = true; // Special when root object is not a trivial Object, it will // be encapsulated in `$`. It will also be encapsulated in // `$` if it has its own `$` property to avoid ambiguity if (types.$ && isPlainObject(types.$)) { obj = obj.$; types = types.$; ignore$Types = false; } /** * * @param {string} type * @param {any} val * @throws {Error} * @returns {any} */ var executeReviver = function executeReviver(type, val) { var _this2$revivers$type; var _ref2 = (_this2$revivers$type = _this2.revivers[type]) !== null && _this2$revivers$type !== void 0 ? _this2$revivers$type : [], _ref3 = _slicedToArray(_ref2, 1), reviver = _ref3[0]; if (!reviver) { throw new Error('Unregistered type: ' + type); } // Only `sync` expected here, as problematic async would // be missing both `revive` and `reviveAsync`, and // encapsulation shouldn't have added types, so // should have made an early exit if (sync && !('revive' in reviver)) { // Just return value as is return val; } if (!sync && reviver.reviveAsync) { return reviver.reviveAsync(val, stateObj); } if (reviver.revive) { return reviver.revive(val, stateObj); } // Shouldn't normally get here throw new Error('Missing reviver'); }; /** * * @throws {Error} Throwing only for TS—not an actual error * @returns {void|TypesonPromise<void>} */ var revivePlainObjects = function revi