typeson
Version:
Preserves types over JSON, BSON or socket.io
1,505 lines (1,416 loc) • 64.8 kB
JavaScript
'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