Socket
Socket
Sign inDemoInstall

typeson

Package Overview
Dependencies
Maintainers
2
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

typeson - npm Package Compare versions

Comparing version 3.2.0 to 4.0.0

CHANGES

2

dist/typeson.js

@@ -1,1 +0,1 @@

!function(r,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Typeson=t():r.Typeson=t()}(this,function(){return function(r){function t(e){if(n[e])return n[e].exports;var o=n[e]={exports:{},id:e,loaded:!1};return r[e].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=r,t.c=n,t.p="",t(0)}([function(r,t){function n(r){var t=[],n={},f=this.types={};this.stringify=function(r,t,n){return JSON.stringify(s(r),t,n)},this.parse=function(r,t){return a(JSON.parse(r,t))};var s=this.encapsulate=function(e,o){function i(r,t,n,e){var o=typeof t;if(o in{string:1,"boolean":1,number:1,undefined:1})return"undefined"===o&&void 0===t||"number"===o&&(isNaN(t)||t===-(1/0)||t===1/0)?c(r,t,e):t;if(null==t)return t;if(n){var p=s.indexOf(t);if(!(0>p))return f[r]="#","#"+a[p];n===!0&&(s.push(t),a.push(r))}var v=t.constructor===Object?t:c(r,t,e);if(v!==t)return v;var d,y=t.constructor===Array;if(t.constructor===Object)d={};else{if(!y)return t;d=new Array(t.length)}if(u(t).forEach(function(e){var o=i(r+(r?".":"")+e,t[e],n,{ownKeys:!0});void 0!==o&&(d[e]=o)}),y)for(var l=0,h=t.length;h>l;l++)if(!(l in t)){var b=i(r+(r?".":"")+l,t[l],n,{ownKeys:!1});void 0!==b&&(d[l]=b)}return d}function c(r,e,o){for(var u=t.length;u--;)if(t[u].test(e,o)){var c=t[u].type;if(n[c]){var s=f[r];f[r]=s?[c].concat(s):c}return i(r,t[u].replace(e,o),p&&"readonly",o)}return e}var f={},s=[],a=[],p=r&&"cyclic"in r?r.cyclic:!0,v=i("",e,p,o||{});if(u(f).length){if(v.constructor!==Object||v.$types)return{$:v,$types:{$:f}};v.$types=f}return v},a=this.revive=function(r){function t(r,s,a){if(!f||"$types"!==r){var p=e[r];if(s&&(s.constructor===Object||s.constructor===Array)){var v=c(s)?new Array(s.length):{};u(s).forEach(function(n){var e=t(r+(r?".":"")+n,s[n],a||v);e instanceof i?v[n]=void 0:void 0!==e&&(v[n]=e)}),s=v}return p?"#"===p?o(a,s.substr(1)):[].concat(p).reduce(function(r,t){var e=n[t];if(!e)throw new Error("Unregistered type: "+t);return e(r)},s):s}}var e=r.$types,f=!0;return e?(e.$&&e.$.constructor===Object&&(r=r.$,e=e.$,f=!1),t("",r)):r};this.register=function(r){return[].concat(r).forEach(function o(r){return c(r)?r.map(o):void(r&&u(r).forEach(function(o){var i=r[o],u=t.filter(function(r){return r.type===o});if(u.length&&(t.splice(t.indexOf(u[0]),1),delete n[o],delete f[o]),i){if("function"==typeof i){var c=i;i=[function(r){return r.constructor===c},function(r){return e({},r)},function(r){return e(Object.create(c.prototype),r)}]}t.push({type:o,test:i[0],replace:i[1]}),i[2]&&(n[o]=i[2]),f[o]=i}}))}),this}}function e(r,t){return u(t).map(function(n){r[n]=t[n]}),r}function o(r,t){if(""===t)return r;var n=t.indexOf(".");if(-1!==n){var e=r[t.substr(0,n)];return void 0===e?void 0:o(e,t.substr(n+1))}return r[t]}function i(){}var u=Object.keys,c=Array.isArray;n.Undefined=i,r.exports=n}])});
!function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.Typeson=n():e.Typeson=n()}(this,function(){return function(e){function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}var t={};return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n(n.s=0)}([function(e,n){function t(e,n){return u.isObject(e)&&"function"==typeof e.then&&(!n||"function"==typeof e.catch)}function r(e){return b.call(e).slice(8,-1)}function i(e,n){if(!e||"object"!=typeof e)return!1;var t=O(e);if(!t)return!1;var r=g.call(t,"constructor")&&t.constructor;return"function"!=typeof r?null===n:"function"==typeof r&&null!==n&&w.call(r)===w.call(n)}function c(e){return!(!e||"Object"!==r(e))&&(!O(e)||i(e,Object))}function o(e){if(!e||"Object"!==r(e))return!1;var n=O(e);return!n||(i(e,Object)||o(n))}function s(e){return e&&"object"==typeof e}function u(e){var n=[],r=[],o={},u=this.types={},y=this.stringify=function(n,t,r,i){i=Object.assign({},e,i,{stringification:!0});var c=O(n,null,i);return d(c)?JSON.stringify(c[0],t,r):c.then(function(e){return JSON.stringify(e,t,r)})};this.stringifySync=function(e,n,t,r){return y(e,n,t,Object.assign({},{throwOnBadSyncType:!0},r,{sync:!0}))},this.stringifyAsync=function(e,n,t,r){return y(e,n,t,Object.assign({},{throwOnBadSyncType:!0},r,{sync:!1}))};var b=this.parse=function(n,t,r){return r=Object.assign({},e,r,{parse:!0}),g(JSON.parse(n,t),r)};this.parseSync=function(e,n,t){return b(e,n,Object.assign({},{throwOnBadSyncType:!0},t,{sync:!0}))},this.parseAsync=function(e,n,t){return b(e,n,Object.assign({},{throwOnBadSyncType:!0},t,{sync:!1}))};var O=this.encapsulate=function(t,u,a){function y(e){return h(g).length?e&&c(e)&&!e.hasOwnProperty("$types")?e.$types=g:e={$:e,$types:{$:g}}:s(e)&&e.hasOwnProperty("$types")&&(e={$:e,$types:!0}),e}function p(e,n){return Promise.all(n.map(function(e){return e[1].p})).then(function(t){return Promise.all(t.map(function(t){var r=[],c=n.splice(0,1)[0],o=c[0],s=c[2],u=c[3],a=c[4],f=c[5],y=c[6],h=l(o,t,s,u,r,!0,y),d=i(h,v);return o&&d?h.p.then(function(n){return a[f]=n,p(e,r)}):(o?a[f]=h:e=d?h.p:h,p(e,r))}))}).then(function(){return e})}function l(e,t,r,o,s,u,a){var y,p={},O=S?function(n){if(S){var c=a||o.type;S(Object.assign(n||p,{keypath:e,value:t,cyclic:r,stateObj:o,promisesData:s,resolvingTypesonPromise:u,awaitingTypesonPromise:i(t,v)},void 0!==c?{type:c}:{}))}}:null,m=typeof t;if(m in{string:1,boolean:1,number:1,undefined:1})return void 0===t||"number"===m&&(isNaN(t)||t===-1/0||t===1/0)?(y=b(e,t,o,s,!1,u))!==t&&(p={replaced:y}):y=t,O&&O(),y;if(null===t)return O&&O(),t;if(r&&!o.iterateIn&&!o.iterateUnsetNumeric){var P=w.indexOf(t);if(!(P<0))return g[e]="#",O&&O({cyclicKeypath:j[P]}),"#"+j[P];!0===r&&(w.push(t),j.push(e))}var T,A=c(t),$=d(t),x=(A||$)&&(!n.length||o.replaced)||o.iterateIn?t:b(e,t,o,s,A||$);if(x!==t?(y=x,p={replaced:x}):$||"array"===o.iterateIn?(T=new Array(t.length),p={clone:T}):A||"object"===o.iterateIn?(T={},p={clone:T}):""===e&&i(t,v)?(s.push([e,t,r,o,void 0,void 0,o.type]),y=t):y=t,O&&O(),!T)return y;if(o.iterateIn){for(var B in t){var E={ownKeys:t.hasOwnProperty(B)},I=e+(e?".":"")+f(B),N=l(I,t[B],!!r,E,s,u);i(N,v)?s.push([I,N,!!r,E,T,B,E.type]):void 0!==N&&(T[B]=N)}O&&O({endIterateIn:!0,end:!0})}else h(t).forEach(function(n){var c=e+(e?".":"")+f(n),o={ownKeys:!0},a=l(c,t[n],!!r,o,s,u);i(a,v)?s.push([c,a,!!r,o,T,n,o.type]):void 0!==a&&(T[n]=a)}),O&&O({endIterateOwn:!0,end:!0});if(o.iterateUnsetNumeric){for(var K=0,k=t.length;K<k;K++)if(!(K in t)){var I=e+(e?".":"")+K,E={ownKeys:!1},N=l(I,void 0,!!r,E,s,u);i(N,v)?s.push([I,N,!!r,E,T,K,E.type]):void 0!==N&&(T[K]=N)}O&&O({endIterateUnsetNumeric:!0,end:!0})}return T}function b(e,t,i,c,s,u){for(var a=s?n:r,f=a.length;f--;){var y=a[f];if(y.test(t,i)){var p=y.type;if(o[p]){var v=g[e];g[e]=v?[p].concat(v):p}if(i=Object.assign(i,{replaced:!0,type:p}),(O||!y.replaceAsync)&&!y.replace)return l(e,t,P&&"readonly",i,c,u,p);return l(e,y[O||!y.replaceAsync?"replace":"replaceAsync"](t,i),P&&"readonly",i,c,u,p)}}return t}a=Object.assign({sync:!0},e,a);var O=a.sync,g={},w=[],j=[],m=[],P=!(a&&"cyclic"in a)||a.cyclic,S=a.encapsulateObserver,T=l("",t,P,u||{},m);return m.length?O&&a.throwOnBadSyncType?function(){throw new TypeError("Sync method requested but async result obtained")}():Promise.resolve(p(T,m)).then(y):!O&&a.throwOnBadSyncType?function(){throw new TypeError("Async method requested but sync result obtained")}():a.stringification&&O?[y(T)]:O?y(T):Promise.resolve(y(T))};this.encapsulateSync=function(e,n,t){return O(e,n,Object.assign({},{throwOnBadSyncType:!0},t,{sync:!0}))},this.encapsulateAsync=function(e,n,t){return O(e,n,Object.assign({},{throwOnBadSyncType:!0},t,{sync:!1}))};var g=this.revive=function(n,r){function s(e,n,t,r,u,b){if(!y||"$types"!==e){var O=a[e];if(d(n)||c(n)){var u=d(n)?new Array(n.length):{};for(h(n).forEach(function(c){var o=s(e+(e?".":"")+f(c),n[c],t||u,r,u,c);i(o,l)?u[c]=void 0:void 0!==o&&(u[c]=o)}),n=u;v.length;){var g=v[0],t=g[0],w=g[1],u=g[2],b=g[3],j=p(t,w);if(i(j,l))u[b]=void 0;else{if(void 0===j)break;u[b]=j}v.splice(0,1)}}if(!O)return n;if("#"===O){var m=p(t,n.substr(1));return void 0===m&&v.push([t,n.substr(1),u,b]),m}var P=r.sync;return[].concat(O).reduce(function(e,n){var t=o[n];if(!t)throw new Error("Unregistered type: "+n);return t[P&&t.revive?"revive":!P&&t.reviveAsync?"reviveAsync":"revive"](e)},n)}}r=Object.assign({sync:!0},e,r);var u=r.sync,a=n&&n.$types,y=!0;if(!a)return n;if(!0===a)return n.$;a.$&&c(a.$)&&(n=n.$,a=a.$,y=!1);var v=[],b=s("",n,null,r);return b=i(b,l)?void 0:b,t(b)?u&&r.throwOnBadSyncType?function(){throw new TypeError("Sync method requested but async result obtained")}():b:!u&&r.throwOnBadSyncType?function(){throw new TypeError("Async method requested but sync result obtained")}():u?b:Promise.resolve(b)};this.reviveSync=function(e,n){return g(e,Object.assign({},{throwOnBadSyncType:!0},n,{sync:!0}))},this.reviveAsync=function(e,n){return g(e,Object.assign({},{throwOnBadSyncType:!0},n,{sync:!1}))},this.register=function(e,t){return t=t||{},[].concat(e).forEach(function e(i){if(d(i))return i.map(e);i&&h(i).forEach(function(e){if("#"===e)throw new TypeError("# cannot be used as a type name as it is reserved for cyclic objects");var c=i[e],s=c.testPlainObjects?n:r,f=s.filter(function(n){return n.type===e});if(f.length&&(s.splice(s.indexOf(f[0]),1),delete o[e],delete u[e]),c){if("function"==typeof c){var y=c;c={test:function(e){return e&&e.constructor===y},replace:function(e){return a({},e)},revive:function(e){return a(Object.create(y.prototype),e)}}}else d(c)&&(c={test:c[0],replace:c[1],revive:c[2]});var p={type:e,test:c.test.bind(c)};c.replace&&(p.replace=c.replace.bind(c)),c.replaceAsync&&(p.replaceAsync=c.replaceAsync.bind(c));var l="number"==typeof t.fallback?t.fallback:t.fallback?0:1/0;if(c.testPlainObjects?n.splice(l,0,p):r.splice(l,0,p),c.revive||c.reviveAsync){var v={};c.revive&&(v.revive=c.revive.bind(c)),c.reviveAsync&&(v.reviveAsync=c.reviveAsync.bind(c)),o[e]=v}u[e]=c}})}),this}}function a(e,n){return h(n).map(function(t){e[t]=n[t]}),e}function f(e){return e.replace(/~/g,"~0").replace(/\./g,"~1")}function y(e){return e.replace(/~1/g,".").replace(/~0/g,"~")}function p(e,n){if(""===n)return e;var t=n.indexOf(".");if(t>-1){var r=e[y(n.substr(0,t))];return void 0===r?void 0:p(r,n.substr(t+1))}return e[y(n)]}function l(){}function v(e){this.p=new Promise(e)}var h=Object.keys,d=Array.isArray,b={}.toString,O=Object.getPrototypeOf,g={}.hasOwnProperty,w=g.toString;v.prototype.then=function(e,n){var t=this;return new v(function(r,i){t.p.then(function(n){r(e?e(n):n)},function(e){t.p.catch(function(e){return n?n(e):Promise.reject(e)}).then(r,i)})})},v.prototype.catch=function(e){return this.then(null,e)},v.resolve=function(e){return new v(function(n){n(e)})},v.reject=function(e){return new v(function(n,t){t(e)})},["all","race"].map(function(e){v[e]=function(n){return new v(function(t,r){Promise[e](n.map(function(e){return e.p})).then(t,r)})}}),u.Undefined=l,u.Promise=v,u.isThenable=t,u.toStringTag=r,u.hasConstructorOf=i,u.isObject=s,u.isPlainObject=c,u.isUserObject=o,u.escapeKeyPathComponent=f,u.unescapeKeyPathComponent=y,u.getByKeyPath=p,e.exports=u}])});
{
"name": "typeson",
"version": "3.2.0",
"version": "4.0.0",
"description": "Preserves types over JSON, BSON or socket.io",

@@ -29,6 +29,6 @@ "main": "typeson.js",

"base64-arraybuffer": "^0.1.5",
"uglify-js": "^2.6.2",
"webpack": "^1.13.1"
"uglify-js": "^3.0.20",
"webpack": "^3.0.0"
},
"tonicExample": "var Typeson = require('typeson');\nvar TSON = new Typeson().register(require('typeson-registry/presets/builtin'));\n\nTSON.stringify({foo: new Date()}, null, 2);"
}

@@ -20,7 +20,7 @@ # typeson.js

JSON can only contain strings, numbers, booleans, arrays and objects. If you want to serialize other types over HTTP, WebSocket, postMessage() or other channel, this module makes it possible to serialize any type over channels that normally only accepts vanilla objects. Typeson adds a metadata property "$types" to the result that maps each non-trivial property to a type name. The type name is a reference to a registered type specification that you need to have the same on both the stringifying and the parsing side.
JSON can only contain strings, numbers, booleans, `null`, arrays and objects. If you want to serialize other types over HTTP, WebSocket, `postMessage()` or other channels, this module makes it possible to serialize any type over channels that normally only accept vanilla objects. Typeson adds a metadata property `$types` to the result that maps each non-trivial property to a type name. (In the case of arrays or encoded primitives, a new object will instead be created with a `$` property that can be preserved by JSON.) The type name is a reference to a registered type specification that you need to have the same on both the stringifying and the parsing side.
## Type Registry
[typeson-registry](https://github.com/dfahlander/typeson-registry) contains encapsulation rules for standard javascript types such as Date, Error, ArrayBuffer, etc. Pick the types you need, use a preset or write your own.
[typeson-registry](https://github.com/dfahlander/typeson-registry) contains encapsulation rules for standard JavaScript types such as `Date`, `Error`, `ArrayBuffer`, etc. Pick the types you need, use a preset or write your own.

@@ -41,3 +41,3 @@ ```js

```
The module `typeson-registry/presets/builtin` is 1.6 kb minizied and gzipped and adds support 32 builtin javascript types: *Date, RegExp, NaN, Infinity, -Infinity, Set, Map, ArrayBuffer, DataView, Uint8Array, Int8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Error, SyntaxError, TypeError, RangeError, ReferenceError, EvalError, URIError, InternalError, Intl.Collator, Intl.DateTimeFormat, Intl.NumberFormat, Object String, Object Number and Object Boolean*.
The module `typeson-registry/presets/builtin` is 1.6 kb minizied and gzipped and adds support 32 builtin JavaScript types: *Date, RegExp, NaN, Infinity, -Infinity, Set, Map, ArrayBuffer, DataView, Uint8Array, Int8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Error, SyntaxError, TypeError, RangeError, ReferenceError, EvalError, URIError, InternalError, Intl.Collator, Intl.DateTimeFormat, Intl.NumberFormat, Object String, Object Number and Object Boolean*.

@@ -54,11 +54,11 @@ ## Compatibility

- Can stringify custom and standard ES5 / ES6 classes.
- Produces standard JSON with an additional "$types" property in case it is needed.
- Produces standard JSON with an additional `$types` property in case it is needed (or a new object if representing a primitive or array at root).
- Resolves cyclic references, such as lists of objects where each object has a reference to the list
- You can register (almost) any type to be stringifyable (serializable) with your typeson instance.
- Output will be identical to that of JSON.stringify() in case your object doesnt contain special types or cyclic references.
- Type specs may encapsulate its type in other registered types. For example, ImageData is encapsulated as `{array: Uint8ClampedArray, width: number, height: number}`, expecting another spec to convert the Uint8ClampedArray. With the [builtin](https://github.com/dfahlander/typeson-registry/blob/master/presets/builtin.js) preset this means it's gonna be converted to base64, but with the [socketio](https://github.com/dfahlander/typeson-registry/blob/master/presets/socketio.js) preset, its gonna be converted to an ArrayBuffer that is left as-is and streamed binary over the WebSocket channel!
- You can register (almost) any type to be stringifiable (serializable) with your typeson instance.
- Output will be identical to that of `JSON.stringify()` in case your object doesnt contain special types or cyclic references.
- Type specs may encapsulate its type in other registered types. For example, `ImageData` is encapsulated as `{array: Uint8ClampedArray, width: number, height: number}`, expecting another spec to convert the `Uint8ClampedArray`. With the [builtin](https://github.com/dfahlander/typeson-registry/blob/master/presets/builtin.js) preset this means it's gonna be converted to base64, but with the [socketio](https://github.com/dfahlander/typeson-registry/blob/master/presets/socketio.js) preset, its gonna be converted to an `ArrayBuffer` that is left as-is and streamed binary over the WebSocket channel!
## Limitations
Since typeson has a synchronous API, it cannot encapsulate and revive async types such as Blob, File or Observable. Encapsulating an async object requires to be able to emit streamed content asynchronically. Remoting libraries could however complement typeson with a streaming channel that handles the emitting of stream content. For example, a remoting library could define a typeson rule that encapsulates an [Observable](https://github.com/zenparsing/es-observable) to an id (string or number for example), then starts subscribing to it and emitting the chunks to the peer as they arrive. The peer could revive the id to an observable that when subscribed to, will listen to the channel for chunks destinated to the encapsulated ID.
Since typeson has a synchronous API, it cannot encapsulate and revive async types such as `Blob`, `File` or `Observable`. Encapsulating an async object requires to be able to emit streamed content asynchronically. Remoting libraries could however complement typeson with a streaming channel that handles the emitting of stream content. For example, a remoting library could define a typeson rule that encapsulates an [Observable](https://github.com/zenparsing/es-observable) to an id (string or number for example), then starts subscribing to it and emitting the chunks to the peer as they arrive. The peer could revive the id to an observable that when subscribed to, will listen to the channel for chunks destinated to the encapsulated ID.

@@ -136,5 +136,5 @@ ## Usage

Socket.io can stream ArrayBuffers as real binary data. This is more efficient than encapsulating it in base64/JSON. Typeson can leave certain types, like ArrayBuffer, untouched, and leave the stringification / binarization part to other libs (use Typeson.encapsulate() and not Typeson.stringify()).
Socket.io can stream `ArrayBuffer`s as real binary data. This is more efficient than encapsulating it in base64/JSON. Typeson can leave certain types, like `ArrayBuffer`, untouched, and leave the stringification / binarization part to other libs (use `Typeson.encapsulate()` and not `Typeson.stringify()`).
What socket.io doesn't do though, is preserving Dates, Errors or your custom types.
What socket.io doesn't do though, is preserve `Date`s, `Error`s or your custom types.

@@ -144,3 +144,3 @@ So to get the best of two worlds:

- Register preset 'typeson-registry/presets/socketio' as well as your custom types.
- Use `Typeson.encapsulate()` to generate an object ready for socket-io emit()
- Use `Typeson.encapsulate()` to generate an object ready for socket-io `emit()`
- Use `Typeson.revive()` to revive the encapsulated object at the other end.

@@ -174,4 +174,5 @@

```
The encapsulate() method will not stringify but just traverse the object and return a simpler structure where certain properties are replaced with a substitue. Resulting object will also have a $types property containing the type metadata.
The `encapsulate()` method will not stringify but just traverse the object and return a simpler structure where certain properties are replaced with a substitute. The resulting object will also have a `$types` property containing the type metadata.
Packing it up at the other end:

@@ -189,7 +190,7 @@

The BSON format can serialize object over a binary channel. It supports just the standard JSON types plus Date, Error and optionally Function. You can use Typeson to encapsulate and revive other types as well with BSON as bearer. Use it the same way as shown above with socket.io.
The BSON format can serialize object over a binary channel. It supports just the standard JSON types plus `Date`, `Error` and optionally `Function`. You can use Typeson to encapsulate and revive other types as well with BSON as bearer. Use it the same way as shown above with socket.io.
### Use with Worker.postMessage()
Web Workers have the `onmessage` and `postMessage()` communication channel that has built-in support for transferring structures using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). It supports Date, ArrayBuffer and many other standard types, but not Errors or your own custom classes. To support Error and custom types over web worker channel, register just the types that are needed (Errors and your custom types), and then use Typeson.encapsulate() before posting message, and Typeson.revive() in the onmessage callback.
Web Workers have the `onmessage` and `postMessage()` communication channel that has built-in support for transferring structures using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). It supports `Date`, `ArrayBuffer`and many other standard types, but not `Error`s or your own custom classes. To support `Error` and custom types over web worker channels, register just the types that are needed (`Error`s and your custom types), and then use `Typeson.encapsulate()` before posting a message, and `Typeson.revive()` in the `onmessage` callback.

@@ -204,3 +205,3 @@ ## API

Creates an instance of Typeson, on which you may configure additional types to support, or call encapsulate(), revive(), stringify() or parse() on.
Creates an instance of Typeson, on which you may configure additional types to support, or call `encapsulate()`, `revive()`, `stringify()` or `parse()` on.

@@ -213,10 +214,61 @@ #### Arguments

{
cyclic?: boolean, // Default true
cyclic?: boolean, // Default true to allow cyclic objects
encapsulateObserver?: function, // Default no-op
sync: true, // Don't force a promise response regardless of type
throwOnBadSyncType: true // Default to throw when mismatch with `Typeson.Promise` obtained for sync request or not returned for async
}
```
###### cyclic
###### `cyclic`: boolean
Whether or not to support cyclic references. Default true unless explicitely set to false. If this property is false, the parsing algorithm becomes a little faster and in case a single object occurs on multiple properties, it will be duplicated in the output (as JSON.stringify() would do). If this property is true, several instances of same object will only occur once in the generated JSON and other references will just contain a pointer to the single reference.
Whether or not to support cyclic references. Defaults to `true` unless explicitly set to `false`. If this property is `false`, the parsing algorithm becomes a little faster and in case a single object occurs on multiple properties, it will be duplicated in the output (as `JSON.stringify()` would do). If this property is `true`, several instances of same object will only occur once in the generated JSON and other references will just contain a pointer to the single reference.
###### `encapsulateObserver`: object (see description)
For encapsulations/stringifications, this callback will be executed as objects are iterated and types are detected. An observer might be used to build an interface based on the original object taking advantage of serialized values (the `replaced` property) passed to the observer along the way, even potentially without concern to the actual encapsulated result.
`encapsulateObserver` is passed an object with the following properties:
- `keypath` - The keypath at which the observer is reporting.
- `value` - The original value found at this stage by the observer. (`replaced`, on the other hand, can be consulted to obtain any type replacement value.)
- `cyclic` - A boolean indicating whether the current state is expecting cyclics. Will be `"readonly"` if this iteration is due to a recursive replacement.
- `stateObj` - The state object at the time of observation.
- `promisesData` - The promises array.
- `resolvingTypesonPromise` - A boolean indicating whether or not this observation is occurring at the (Typeson) promise stage.
- `awaitingTypesonPromise` - Will be `true` if still awaiting the full resolution; this could be ignored or used to set a placeholder.
The following properties are also present in particular cases:
- `type` - If a type was detected, whether at either the `awaitingTypesonPromise` or `resolvingTypesonPromise` stage, this property will indicate the detected type.
- `clone` - If a plain object or array is found or if `iterateIn` is set, this property holds the clone of that object or array.
- `replaced` - This property will be set when a type was detected. This value is useful for obtaining the serialization of types.
- `cyclicKeypath` - Will be present if a cyclic object (including array) were detected; refers to the key path of the prior detected object.
- `endIterateIn` - Will be `true` if finishing iteration of `in` properties.
- `endIterateOwn` - Will be `true` if finishing iteration of "own" properties.
- `endIterateUnsetNumeric` - Will be `true` if finishing iteration of unset numeric properties.
- `end` - Convenience property that will be `true` if `endIterateIn`, `endIterateOwn`, or `endIterateUnsetNumeric` is `true`.
###### `sync`: boolean (Internal property)
Types can utilize `Typeson.Promise` to allow asynchronous encapsulation and stringification.
When such a type returns a `Typeson.Promise`, a regular `Promise` will be returned to the user.
(This property is used internally for ensuring a regular `Promise` was not intended as the result.
Note that its resolved value is also recursively checked for types.)
To ensure that a regular `Promise` is always returned and thereby to allow the same API to be
used regardless of the types in effect, the `sync` option is set to `false` by the
`*Async` methods.
Note that this has no bearing on `revive`/`parse` since they can construct any object they
wish for a return value, including a `Promise`, a stream, etc.
###### `throwOnBadSyncType`: boolean
The default is to throw when an async result is received from a synchronous method or vice versa.
This assures you that you are receiving the intended result type.
This option can be set to `false`, however, to return the raw synchronous result or the promise, allowing you the least unambiguous results (since you can discern whether a returned `Promise` was
the actual result of a revival/parsing or just the inevitable return of using an async method).
#### Sample

@@ -232,3 +284,2 @@

var obj = typeson.parse(tson);
```

@@ -238,5 +289,5 @@

#### types
#### `types`
A map between type identifyer and type-rules. Same structure as passed to register(). Use this property if you want to create a new Typeson containing all types from another Typeson.
A map between type identifier and type-rules. Same (object-based) structure as passed to `register()`. Use this property if you want to create a new Typeson containing all types from another Typeson.

@@ -256,11 +307,24 @@ ##### Sample

### Methods
### Instace methods
#### stringify (obj, [replacer], [space])
#### `stringify` (obj, [replacer], [space], [options])
*Arguments identical to those of JSON.stringify()*
*Initial arguments identical to those of JSON.stringify()*
Generates JSON based on given obj. If given obj has special types or cyclic references, the produce JSON will contain a $types property on the root where type info relies.
Generates JSON based on the given `obj`. Applies `JSON.stringify()` on the result of any relevant `replace` encapsulators.
If the supplied `obj` has special types or cyclic references, the produced JSON will contain a `$types` property on the root upon which type info relies (a map of keypath to type where the keypath is dot-separated; see `Typeson.escapeKeyPathComponent` on escaping).
The `options` object argument can include a setting for `cyclic` which overrides the default or any behavior supplied for this option in the Typeson constructor.
May also return a `Promise` if a type's `replace` encapsulator returns `Typeson.Promise`. See the documentation under `Typeson.Promise`.
##### Stringification format
If enabled, the cyclic "type" will be represented as `#` and cyclic references will be encoded as `#` plus the path to the referenced object.
If an array or primitive is encoded at root, an object will be created with a property `$` and a `$types` property that is an object with `$` as a key and instead of a type string as value, a keypath-type object will be its value (with the empty string indicating the root path).
##### Sample
```js

@@ -275,8 +339,21 @@ var TSON = new Typeson().register(require('typeson-registry/types/date'));

#### parse (obj, [reviver])
#### `stringifySync` (obj, [replacer], [space], [options])
As with `stringify` but automatically throws upon obtaining a `Typeson.Promise` return result from a `replace` encapsulator (as that is expected for asynchronous types).
#### `stringifyAsync` (obj, [replacer], [space], [options])
As with `stringify` but automatically throws upon obtaining a non-`Typeson.Promise` return result from a `replace` encapsulator (as only a `Typeson.Promise` is expected for asynchronous types).
#### `parse` (obj, [reviver])
*Arguments identical to those of JSON.parse()*
Parses Typeson genereted JSON back into the original complex structure again.
Parses Typeson-generated JSON back into the original complex structure again.
Applies `JSON.parse()` and then any relevant `revive` methods that are detected.
May also return a `Promise` if a type's reviver returns `Typeson.Promise`. See
the documentation under `Typeson.Promise`.
##### Sample

@@ -289,6 +366,27 @@

#### encapsulate (obj)
#### `parseSync` (obj, [reviver])
Encapsulates an object but leaves the stringification part to you. Pass your encapsulated object further to socket.io, postMessage(), BSON or indexedDB.
As with `parse` but automatically throws upon obtaining a `Typeson.Promise` return result from the reviver (as that is expected for asynchronous types).
#### `parseAsync` (obj, [reviver])
As with `parse` but automatically throws upon obtaining a non-`Typeson.Promise` return result from the reviver (as only a `Typeson.Promise` is expected for asynchronous types).
#### `encapsulate` (obj, [stateObj], [opts])
Encapsulates an object but leaves the stringification part to you. Pass your encapsulated object further to socket.io, `postMessage()`, BSON or IndexedDB.
Applies the `replace` method on `test`-matching spec objects. Will return the result regardless
of whether it is an asynchronous (indicated by a `Typeson.Promise`) or synchronous result.
The `options` object argument can include a setting for `cyclic` which overrides the default or any behavior supplied for this option in the Typeson constructor.
#### `encapsulateSync` (obj, [opts])
As with `encapsulate` but automatically throws upon obtaining a `Typeson.Promise` return result from the replacer (as that is expected for asynchronous types).
#### `encapsulateAsync` (obj, [opts])
As with `encapsulate` but automatically throws upon obtaining a non-`Typeson.Promise` return result from the replacer (as only a `Typeson-Promise` is expected for asynchronous types).
##### Sample

@@ -302,14 +400,29 @@

#### revive (obj)
#### `revive` / `reviveSync` / `reviveAsync` (obj)
Revives an encapsulated object. See encapsulate().
Revives an encapsulated object. See `encapsulate()`.
#### register (typeSpec)
#### `register` (typeSpec, opts = {fallback: boolean|number})
If `opts.fallback` is set, lower priority will be given (the default is that the last registered item
has highest priority during match testing). If a number is given, it will be used as the index of the placement.
##### typeSpec
An object that maps a type-name to a specification of how to test,encapsulate and revive that type.
An object that maps a type-name to a specification of how to test, encapsulate and revive that type.
`{TypeName => constructor-function | [tester, encapsulator, reviver]}` or an array of such structure.
`{TypeName => constructor-function | [tester, encapsulator, reviver] | specObject = {test: function, replace: function, revive: function, testPlainObjects: boolean=false}}` or an array of such structures.
Please note that if an array is supplied, the tester (and upon matching, the encapsulator)
execute in a last-in, first out order. (Calls to `register` can set `fallback` to `true` to
lower the priority of a recent addition.)
`this` will refer to the specification object.
Subsequent calls to `register` will similarly be given higher priority so be sure to add
catch-all matchers *before* more precise ones.
If `testPlainObjects` is set to `true`, a tester will be checked against plain objects and
allow replacements without recursion.
###### constructor-function

@@ -319,7 +432,8 @@

- test: check if x.constructor === constructor-function.
- encapsulate: copy all enumerable own props into a vanilla object
- revive: Use Object.create() to revive the correct type, and copy all props into it.
- `test`: check if x.constructor === constructor-function.
- `replace`: copy all enumerable own props into a vanilla object
- `revive`: Uses `Object.create()` to revive the correct type and copies all properties into it.
- `testPlainObjects`: `false`: Tests non-plain objects only.
###### tester (obj : any, stateObj : {ownKeys: boolean}) : boolean
###### `test` (obj : any, stateObj : {ownKeys: boolean, iterateIn: ('array'|'object'), iterateUnsetNumeric: boolean}) : boolean

@@ -336,10 +450,38 @@ Function that tests whether an instance is of your type and returns a truthy value if it is.

###### encapsulator (obj: YourType, stateObj : {ownKeys: boolean}) : Object
You may also set values on the state object.
Function that maps your instance to a JSON-serializable object. For the `stateObj`,
see `tester`. In a property context (for arrays or objects), returning `undefined`
will prevent the addition of the property.
Normally, only the "own" keys of an object will be iterated.
Setting `iterateIn` changes the behavior to iterate all properties
"in" the object for cloning (though note that doing so will add a
performance cost). The value of `iterateIn` (as 'array' or 'object')
determines what type of object will be created. Normally, 'object'
will be more useful as non-array-index properties do not
survive stringification on an array.
###### reviver (obj: Object) : YourType
One special case not covered by iterating all "own" keys or enabling "in"
iteration is where one may wish to iterate the keys not "in" the object
but still part of it, i.e., the unset numeric indexes of a sparse array
(e.g., for the sake of ensuring they are ignored entirely rather than
converted to `null` by a `stringify` call). Thus encapsulators have the
ability to set `iterateUnsetNumeric: true` on their state object, but
note that doing so will add a performance cost.
###### `replace` (obj: YourType, stateObj : {ownKeys: boolean, iterateIn: ('array'|'object'), iterateUnsetNumeric: boolean}) : Object
Function that maps your instance to a JSON-serializable object. Can also be called an
`encapsulator`. For the `stateObj`, see `tester`. In a property context (for arrays
or objects), returning `undefined` will prevent the addition of the property.
See the `tester` for a discussion of the `stateObj`.
Note that replacement results will themselves be recursed for state changes
and type detection.
###### `replaceAsync` (obj: YourType, stateObj : {ownKeys: boolean, iterateIn: ('array'|'object'), iterateUnsetNumeric: boolean}) : `Typeson.Promise`
Expected to return a `Typeson.Promise` which resolves to the replaced value.
See `replace`.
###### `revive` (obj: Object) : YourType
Function that maps your JSON-serializable object into a real instance of your type.

@@ -387,4 +529,11 @@ In a property context (for arrays or objects), returning `undefined`

### `Typeson.Undefined` class
###### `reviveAsync` (obj: Object) : YourType
Expected to return a `Typeson.Promise` which resolves to the revived value.
See `revive`.
### Class methods
#### `Typeson.Undefined` class
During encapsulation, `undefined` will not be set for property values,

@@ -404,2 +553,114 @@ of objects or arrays (including sparse ones and replaced values)

[typeson-registry](https://github.com/dfahlander/typeson-registry) contains ready-to-use types to register with your Typeson instance.
#### `Typeson.Promise` class
If you have a type which you wish to have resolved asynchronously, you
can can return a `Typeson.Promise` (which works otherwise like a `Promise`)
and call its first supplied argument (`resolve`) when ready.
The reason we expect this class to be used here instead of regular `Promise`s
as types might wish to serialize them in their own manner (or perhaps more
likely, to be able to throw when encountering them if they
are not expected).
##### Sample
```js
function MyAsync (prop) {
this.prop = prop;
}
var typeson = new Typeson({sync: false}).register({
myAsyncType: [
function (x) { return x instanceof MyAsync;},
function (o) {
return new Typeson.Promise(function (resolve, reject) {
setTimeout(function () { // Do something more useful in real code
resolve(o.prop);
}, 800);
});
},
function (data) {
return new MyAsync(data);
}
]
});
var mya = new MyAsync(500);
return typeson.stringify(mya).then(function (result) {
var back = typeson.parse(result, null, {sync: true});
console.log(back.prop); // 500
});
```
#### `Typeson.toStringTag`
A simple utility for getting the former ``[[Class]]`` internal slot of an object
(i.e., The string between `[Object ` and `]` as returned from
`Object.prototype.toString`) or what is known in HTML as the ["class string"](https://heycam.github.io/webidl/#dfn-class-string).
Since [`Symbol.toStringTag`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag)
can set the value for other objects and is defined by JavaScript itself, we
use that within the method name.
The method can be used for cross-frame detection of your objects as well
as objects associated with all
[platform objects](https://heycam.github.io/webidl/#idl-objects)
(i.e., non-callback interfaces or `DOMException`s) tied to WebIDL
(such as the interfaces in HTML). The platform object's [identifier](https://heycam.github.io/webidl/#es-platform-objects) (i.e., the
interface name) is, per the WebIDL spec, the string to be returned.
Although it is unfortunately not immune to forgery, it may in some
cases be more appealing than (or usable in addition to) duck typing
so this tiny utility is bundled for convenience.
#### `Typeson.hasConstructorOf` (objWithPrototypeConstructor, classToCompare: constructor or null) : boolean
Another approach for class comparisons involves checking a `constructor`
function and comparing its `toString`. This is required for some classes
which otherwise do not define `toStringTag`s which differ from other
objects. The first argument will be an object to check (whose prototoype
will be searched for a `constructor` property) whereas the second is a
class constructor to compare.
If no valid `constructor` is found, `false` will be returned unless
`null` was supplied as the `classToCompare` in which case `true` will
be returned.
#### `Typeson.isObject` (val)
Simple but frequently-needed type-checking utility for
`val && typeof val === 'object'` to avoid `null` being treated as an object.
#### `Typeson.isPlainObject` (val)
Checks for a simple non-inherited object. Adapted from jQuery's `isPlainObject`.
#### `Typeson.isUserObject` (val)
Allows for inherited objects but ensures the prototype chain inherits from
`Object` (or `null`).
#### `Typeson.isThenable` (val, catchCheck=boolean)
Checks whether an object is "thenable" (usable as a promise). If the second
argument is supplied as `true`, it will also ensure it has a `catch` method.
A regular `Promise` or `Typeson.Promise` will return `true`.
#### `Typeson.escapeKeyPathComponent` (unescapedKeyPathComponent)
Escapes a component of a key path.
Dots in property names are escaped as `~1`, and the tilde escape character is
itself escaped as `~0`.
#### `Typeson.unescapeKeyPathComponent` (escapedKeyPathComponent)
Unescapes a key path component. See `Typeson.escapeKeyPathComponent`.
#### `Typeson.getByKeyPath` (obj, keyPath)
Retrieves a value pointed to by a key path on an object.
## Finding types and groups of types
[typeson-registry](https://github.com/dfahlander/typeson-registry) contains ready-to-use types and presets to register with your Typeson instances.

@@ -25,3 +25,3 @@ var Typeson = require('./typeson');

ArrayBuffer: [
function test (x) { return x.constructor === ArrayBuffer;},
function test (x) { return Typeson.toStringTag(x) === 'ArrayBuffer'; },
function encapsulate (b) { return B64.encode(b); },

@@ -44,7 +44,20 @@ function revive (b64) { return B64.decode(b64); }

};
function run(tests){tests.forEach(function(test){
function run(tests){
if (!tests.length) {
return;
}
var test = tests.splice(0, 1)[0];
console.log(" ");
console.log("Running test: " + test.name);
test();
})}
var ret = test();
if (Typeson.isThenable(ret)) {
ret.then(function () {
run(tests);
}).catch(function (err) {
console.log('Promise error in test ' + test.name + ': \n\t' + err);
});
} else {
run(tests);
}
}
function roundtrip(x) {

@@ -56,3 +69,3 @@ var tson = typeson.stringify(x, null, 2);

run ([function shouldSupportBasicTypes () {
run([function shouldSupportBasicTypes () {
//

@@ -64,3 +77,5 @@ // shouldSupportBasicTypes

var date = new Date();
res = roundtrip({a: "a", b: 2, c: function(){}, d: false, e: Symbol(), f: [], g: date, h: /apa/gi });
var input = {a: "a", b: 2, c: function(){}, d: false, e: null, f: Symbol(), g: [], h: date, i: /apa/gi };
res = roundtrip(input);
assert (res !== input, "Object is a clone, not a reference");
assert (res.a === "a", "String value");

@@ -70,6 +85,187 @@ assert (res.b === 2, "Number value");

assert (res.d === false, "Boolean value");
assert (!res.e, "Symbols should not follow by default");
assert (Array.isArray(res.f) && res.f.length === 0, "Array value");
assert (res.g instanceof Date && res.g.toString() == date.toString(), "Date value");
assert (res.e === null, "Null value");
assert (!res.f, "Symbols should not follow by default");
assert (Array.isArray(res.g) && res.g.length === 0, "Array value");
assert (res.h instanceof Date && res.h.toString() == date.toString(), "Date value");
assert (Object.keys(res.i).length === 0, "regex only treated as empty object by default");
}, function shouldResolveNestedObjects () {
var input = {a: [{subA: 5}, [6, 7]], b: {subB: {c: 8}} };
res = roundtrip(input);
assert (res.a[0].subA === 5, "Object within array");
assert (res.a[1][0] === 6, "Array within array");
assert (res.a[1][1] === 7, "Array within array");
assert (res.b.subB.c === 8, "Object within object");
}, function shouldSupportObjectAPI () {
var typeson = new Typeson().register({
Date: {
test: function (x) { return x instanceof Date; },
replace: function (date) { return date.getTime(); },
revive: function (time) { return new Date(time); }
}
});
var date = new Date();
var tson = typeson.stringify(date, null, 2);
//console.log(tson);
var back = typeson.parse(tson);
assert (back instanceof Date && back.toString() == date.toString(), "Date value");
}, function shouldSupportObjectsContainingInternallyUsedProperties () {
function test (data, cb) {
var tson = typeson.stringify(data, null, 2);
console.log(tson);
var result = typeson.parse(tson);
cb(result);
}
function valSwitch (val) {
test({$types: val}, function (result) {
assert(result.$types === val && Object.keys(result).length === 1, "Preserves $types on original object without additions");
});
test({$: val}, function (result) {
assert(result.$ === val && Object.keys(result).length === 1, "Preserves $ on original object without additions");
});
test({$: val, $types: val}, function (result) {
assert(result.$ === val && result.$types === val && Object.keys(result).length === 2, "Preserves $ and $types on original object without additions");
});
}
valSwitch(true);
valSwitch(false);
test({$: {}, $types: {$: {'': 'val', 'cyc': '#'}, '#': 'a1', '': 'b1'}}, function (result) {
assert(typeof result.$ === 'object' && !Object.keys(result.$).length && result.$types.$[''] === 'val' && result.$types.$.cyc === '#' && result.$types['#'] === 'a1' && result.$types[''] === 'b1' && Object.keys(result.$types).length === 3, "Preserves $ and $types on original object without additions");
});
test({a: new Date(), $types: {}}, function (result) {
assert(result.a instanceof Date && !('$' in result) && typeof result.$types === 'object' && !Object.keys(result.$types).length);
});
test({a: new Date(), $: {}}, function (result) {
assert(result.a instanceof Date && !('$types' in result) && typeof result.$ === 'object' && !Object.keys(result.$).length);
});
test({a: new Date(), $types: {}, $: {}}, function (result) {
assert(result.a instanceof Date && typeof result.$types === 'object' && !Object.keys(result.$types).length && typeof result.$ === 'object' && !Object.keys(result.$).length);
});
function valSwitch2 (val) {
test({a: new Date(), $types: val}, function (result) {
assert(result.a instanceof Date && !('$' in result) && result.$types === val);
});
test({a: new Date(), $: val}, function (result) {
assert(result.a instanceof Date && !('$types' in result) && result.$ === val);
});
test({a: new Date(), $types: val, $: val}, function (result) {
assert(result.a instanceof Date && result.$types === val && result.$ === val);
});
}
valSwitch2(true);
valSwitch2(false);
test({a: new Date(), $: {}}, function (result) {
assert(result.a instanceof Date && !('$types' in result) && typeof result.$ === 'object' && !Object.keys(result.$).length);
});
}, function disallowsHashType () {
var caught = false;
try {
var typeson = new Typeson().register({'#': [function () {}, function () {}, function () {}]})
} catch (err) {
caught = true;
}
assert(caught, "Should throw on attempting to register the reserved 'type', '#'");
}, function shouldHandlePathSeparatorsInObjects () {
var input = {
'aaa': {
bbb: new Date(91000000000)
},
'aaa.bbb': 2,
'lll': {
mmm: 3
},
'lll.mmm': new Date(92000000000),
'qqq.rrr': 4,
'qqq': {
rrr: new Date(93000000000)
},
'yyy': {
zzz: 5
},
'yyy.zzz': new Date(94000000000),
'allNormal1': {
a: 100
},
'allNormal1.a': 200,
'allTyped1': {
a: new Date(95000000000)
},
'allTyped1.a': new Date(96000000000),
'allNormal2.b': 400,
'allNormal2': {
b: 500
},
'allTyped2': {
b: new Date(97000000000)
},
'allTyped2.b': new Date(98000000000),
'A~': 'abc',
'A~1': 'ghi',
'A~0': 'def',
'A.': 'jkl',
'B~': new Date(99100000000),
'B~0': new Date(99200000000),
'B~1': new Date(99300000000),
'B.': new Date(99400000000),
};
var res = roundtrip(input);
assert(
res.aaa.bbb instanceof Date && res['aaa.bbb'] === 2 &&
res.aaa.bbb.getTime() === 91000000000,
"Properties with periods (with type) after normal properties (without type)"
);
assert(
res['lll.mmm'] instanceof Date && res.lll.mmm === 3 &&
res['lll.mmm'].getTime() === 92000000000,
"Properties with periods (without type) after normal properties (with type)"
);
assert(
res.qqq.rrr instanceof Date && res['qqq.rrr'] === 4 &&
res.qqq.rrr.getTime() === 93000000000,
"Properties with periods (without type) before normal properties (with type)"
);
assert(
res['yyy.zzz'] instanceof Date && res.yyy.zzz === 5 &&
res['yyy.zzz'].getTime() === 94000000000,
"Properties with periods (with type) before normal properties (without type)"
);
assert(
res.allNormal1.a === 100 && res['allNormal1.a'] === 200,
"Properties with periods (without type) after normal properties (without type)"
);
assert(
res.allTyped1.a instanceof Date && res['allTyped1.a'] instanceof Date &&
res.allTyped1.a.getTime() === 95000000000 &&
res['allTyped1.a'].getTime() === 96000000000,
"Properties with periods (with type) after normal properties (with type)"
);
assert(
res.allNormal2.b === 500 && res['allNormal2.b'] === 400,
"Properties with periods (without type) before normal properties (without type)"
);
assert(
res.allTyped2.b instanceof Date && res['allTyped2.b'] instanceof Date &&
res.allTyped2.b.getTime() === 97000000000 &&
res['allTyped2.b'].getTime() === 98000000000,
"Properties with periods (with type) after normal properties (with type)"
);
assert(
res['A~'] === 'abc' &&
res['A~0'] === 'def' &&
res['A~1'] === 'ghi' &&
res['A.'] === 'jkl' &&
res['B~'] instanceof Date && res['B~'].getTime() === 99100000000 &&
res['B~0'] instanceof Date && res['B~0'].getTime() === 99200000000 &&
res['B~1'] instanceof Date && res['B~1'].getTime() === 99300000000 &&
res['B.'] instanceof Date && res['B.'].getTime() === 99400000000,
"Find properties with escaped and unescaped characters"
);
}, function shouldResolveCyclics() {

@@ -90,3 +286,3 @@ //

var tson = typeson.stringify(data,null, 2);
var tson = typeson.stringify(data, null, 2);
//console.log(tson);

@@ -109,6 +305,38 @@ var result = typeson.parse(tson);

console.log(tson);
assert (tson.match(/Kalle/g).length === 1, "TSON should only contain one 'Kalle'. The other should just reference the first");
assert (tson.match(/Kalle/g).length === 1, "TSON should only contain one 'Kalle'. The others should just reference the first");
var result = typeson.parse(tson);
assert (result[0] === result[1] && result[1] === result[2], "The resulting object should also just have references to the same object");
}, function shouldResolveCyclicArrays () {
var recursive = [];
recursive.push(recursive);
var tson = typeson.stringify(recursive);
var result = typeson.parse(tson);
assert(result === result[0], "array directly contains self");
var recursive2 = [];
recursive2.push([recursive2]);
tson = typeson.stringify(recursive2);
result = typeson.parse(tson);
assert(result !== result[0] && result === result[0][0], "array indirectly contains self");
var recursive3 = [recursive];
tson = typeson.stringify(recursive3);
console.log(tson);
result = typeson.parse(tson);
assert(result !== result[0] && result !== result[0][0] && result[0] === result[0][0], "array member contains self");
var recursive4 = [1, recursive];
tson = typeson.stringify(recursive4);
console.log(tson);
result = typeson.parse(tson);
assert(result !== result[1] && result !== result[1][0] && result[1] === result[1][0], "array member contains self");
}, function shouldResolveCyclicObjectMembers () {
var recursive = {};
recursive.b = recursive;
var recursiveContainer = {a: recursive};
tson = typeson.stringify(recursiveContainer);
console.log(tson);
result = typeson.parse(tson);
assert(result !== result.a && result !== result.b && result.a === result.a.b, "Object property contains self");
}, function shouldNotResolveCyclicsIfNotWanted(){

@@ -187,3 +415,3 @@ //

});
var tson = typeson.stringify(input,null, 2);
var tson = typeson.stringify(input, null, 2);
console.log(tson);

@@ -288,2 +516,491 @@ var result = typeson.parse(tson);

assert (back.hello === "world", "Should have all properties there.");
}, function shouldExecuteReplacersInProperOrder () {
function Person () {}
var john = new Person();
var typeson = new Typeson().register([
{specificClassFinder: [(x) => x instanceof Person, () => 'specific found']},
{genericClassFinder: [(x) => x && typeof x === 'object', () => 'general found']}
]);
var clonedData = typeson.parse(typeson.stringify(john));
// Todo: Change the expected result to "specific found" if reimplementing in non-reverse order
assert(clonedData === "general found", "Should execute replacers in proper order");
}, function shouldRunEncapsulateObserverSync () {
var expected = '{\n' +
' time: 959000000000\n' +
' vals: [\n' +
' 0: null\n' +
' 1: undefined\n' +
' 2: 5\n' +
' 3: str\n' +
' ]\n' +
' cyclicInput: #\n' +
'}\n';
var str = '';
var indentFactor = 0;
var indent = function () {
return new Array(indentFactor * 4 + 1).join(' ');
};
var typeson = new Typeson({
encapsulateObserver: function (o) {
const isObject = o.value && typeof o.value === 'object';
const isArray = Array.isArray(o.value);
if (o.end) {
indentFactor--;
str += indent() + (isArray ? ']' : '}') + '\n';
return;
}
if (!('replaced' in o)) {
if (isArray) {
if (!('clone' in o)) {
return;
}
str += indent() + (o.keypath ? o.keypath + ': ' : '') + '[\n';
indentFactor++;
return;
}
if (isObject) {
if ('cyclicKeypath' in o) {
o.value = '#' + o.cyclicKeypath;
} else {
str += indent() + '{\n';
indentFactor++;
return;
}
}
} else if (isObject) {
// Special type that hasn't been finally resolved yet
return;
}
var idx = o.keypath.lastIndexOf('.') + 1;
str += indent() + o.keypath.slice(idx) + ': ' +
('replaced' in o ? o.replaced : o.value) + '\n';
}
})
.register(globalTypeson.types);
var input = {
time: new Date(959000000000),
vals: [null, undefined, 5, "str"]
};
input.cyclicInput = input;
var tson = typeson.encapsulate(input);
// console.log(str);
// console.log(expected);
assert (str === expected, "Observer able to reduce JSON to expected string");
}, function shouldRunEncapsulateObserverAsync () {
var expected = '';
var str = '';
var placeholderText = '(Please wait for the value...)';
function APromiseUser (a) {this.a = a;}
var typeson = new Typeson({
encapsulateObserver: function (o) {
const isObject = o.value && typeof o.value === 'object';
const isArray = Array.isArray(o.value);
if (o.resolvingTypesonPromise) {
var idx = str.indexOf(placeholderText);
var start = str.slice(0, idx);
var end = str.slice(idx + placeholderText.length);
str = start + o.value + end;
} else if (o.awaitingTypesonPromise) {
str += '<span>' + placeholderText + '</span>';
} else if (!isObject && !isArray) {
str += '<span>' + o.value + '</span>';
}
}
}).register({
Date: [
function (x) { return x instanceof Date; },
function (date) { return date.getTime(); },
function (time) { return new Date(time); }
],
PromiseUser: [
function (x) { return x instanceof APromiseUser; },
function (o) { return new Typeson.Promise(function (res) {
setTimeout(function () {
res(o.a);
}, 300);
})},
function (val) { return new APromiseUser(val) }
]
});
var input = ['aaa', new APromiseUser(5), 'bbb'];
var prom = typeson.encapsulateAsync(input).then(function (encaps) {
var back = typeson.parse(JSON.stringify(encaps));
assert(
back[0] === input[0] &&
back[2] === input[2] &&
back[1] instanceof APromiseUser &&
back[1].a === 5,
"Should have resolved the one nested promise value");
// console.log(str);
assert(
str === '<span>aaa</span><span>5</span><span>bbb</span>',
"Should have allowed us to run the callback asynchronously (where we can substitute a placeholder)"
);
});
assert(
str === '<span>aaa</span><span>' + placeholderText + '</span><span>bbb</span>',
"Should have allowed us to run the callback synchronously (where we add a placeholder)"
);
return prom;
}, function shouldAllowIterateIn () {
function A (a) {
this.a = a;
}
function createExtendingClass (a) {
function B (b, isArr) {
this[3] = 4;
this.b = b;
this.isArr = isArr;
}
B.prototype = new A(a);
return B;
}
var typeson = new Typeson().register({
iterateIn: {
test: function (x, stateObj) {
if (x instanceof A) {
stateObj.iterateIn = x.isArr ? 'array' : 'object';
return true;
}
return false;
}
}
});
var B = createExtendingClass(5);
var b = new B(7);
var tson = typeson.stringify(b);
console.log(tson);
var back = typeson.parse(tson);
assert(!Array.isArray(back), "Is not an array");
assert(back[3] === 4, "Has numeric property");
assert(back.a === 5, "Got inherited 'a' property");
assert(back.b === 7, "Got own 'b' property");
var b = new B(8, true);
var tson = typeson.stringify(b);
console.log(tson);
var back = typeson.parse(tson);
assert(Array.isArray(back), "Is an array");
assert(back[3] === 4, "Has numeric property");
assert(!('a' in back), "'a' property won't survive array stringification");
assert(!('b' in back), "'b' property won't survive array stringification");
}, function executingToJSON () {
function A () {}
A.prototype.toJSON = function () {return 'abcd';}
var typeson = new Typeson();
var a = new A(); // Encapsulated as is
var tson = typeson.stringify(a);
console.log(tson);
var back = typeson.parse(tson);
assert(back === 'abcd', "Should have executed `toJSON`");
var typeson = new Typeson();
var a = { // Plain object rebuilt during encapsulation including with `toJSON`
toJSON: function () {return 'abcd';}
};
var tson = typeson.stringify(a);
console.log(tson);
var back = typeson.parse(tson);
assert(back === 'abcd', "Should have executed `toJSON`");
}, function shouldAllowPlainObjectReplacements () {
var typeson = new Typeson().register({
plainObj: {
testPlainObjects: true,
test: function (x) {
return 'nonenum' in x;
},
replace: function (o) {
return {
b: o.b,
nonenum: o.nonenum
};
}
}
});
var a = {b: 5};
Object.defineProperty(a, 'nonenum', {
enumerable: false,
value: 100
});
var tson = typeson.stringify(a);
console.log(tson);
var back = typeson.parse(tson);
assert(back.b === 5, "Should have kept property");
assert(back.nonenum === 100, "Should have kept non-enumerable property");
assert(Object.keys(back).includes('nonenum'), "Non-enumerable property should now be enumerable");
}, function shouldAllowSinglePromiseResolution() {
var typeson = new Typeson();
var x = new Typeson.Promise(function (res) {
setTimeout(function () {
res(25);
}, 500);
});
return typeson.stringifyAsync(x).then(function (tson) {
console.log(tson);
var back = typeson.parse(tson);
assert(back === 25, "Should have resolved the one promise value");
});
}, function shouldAllowSingleNestedPromiseResolution() {
function APromiseUser (a) {this.a = a;}
var typeson = new Typeson().register({
Date: [
function (x) { return x instanceof Date; },
function (date) { return date.getTime(); },
function (time) { return new Date(time); }
],
PromiseUser: [
function (x) { return x instanceof APromiseUser; },
function (o) { return new Typeson.Promise(function (res) {
setTimeout(function () {
res(o.a);
}, 300);
})},
function (val) { return new APromiseUser(val) }
]
});
var x = new Typeson.Promise(function (res) {
setTimeout(function () {
res(new APromiseUser(555));
}, 1200);
});
return typeson.stringifyAsync(x).then(function (tson) {
console.log(tson);
var back = typeson.parse(tson);
assert(
back instanceof APromiseUser &&
back.a === 555,
"Should have resolved the one nested promise value");
});
}, function shouldAllowMultiplePromiseResolution() {
var typeson = new Typeson();
var x = [Typeson.Promise.resolve(5), 100, new Typeson.Promise(function (res) {
setTimeout(function () {
res(25);
}, 500);
})];
return typeson.stringifyAsync(x).then(function (tson) {
console.log(tson);
var back = typeson.parse(tson);
assert(back[0] === 5 && back[1] === 100 && back[2] === 25, "Should have resolved multiple promise values (and in the proper order)");
});
}, function shouldAllowNestedPromiseResolution () {
function APromiseUser (a) {this.a = a;}
var typeson = new Typeson().register({
Date: [
function (x) { return x instanceof Date; },
function (date) { return date.getTime(); },
function (time) { return new Date(time); }
],
PromiseUser: [
function (x) { return x instanceof APromiseUser; },
function (o) { return new Typeson.Promise(function (res) {
setTimeout(function () {
res(o.a);
}, 300);
})},
function (val) { return new APromiseUser(val) }
]
});
var x = [
Typeson.Promise.resolve(5),
100,
new Typeson.Promise(function (res) {
setTimeout(function () {
res(25);
}, 500);
}),
new Typeson.Promise(function (res) {
setTimeout(function () {
res(Typeson.Promise.resolve(5));
});
}).then(function (r) {
return new Typeson.Promise(function (res) {
setTimeout(function () {
res(r + 90);
}, 10);
});
}),
Typeson.Promise.resolve(new Date()),
new Typeson.Promise (function (res) {
setTimeout(function () {
res(new APromiseUser(555));
});
})
];
return typeson.stringifyAsync(x).then(function (tson) {
console.log(tson);
var back = typeson.parse(tson);
assert(
back[0] === 5 &&
back[1] === 100 &&
back[2] === 25 &&
back[3] === 95 &&
back[4] instanceof Date &&
back[5] instanceof APromiseUser &&
back[5].a === 555,
"Should have resolved multiple nested promise values (and in the proper order)"
);
});
}, function shouldAllowForcingOfAsyncReturn () {
var typeson = new Typeson({sync: false, throwOnBadSyncType: false});
var x = 5;
return typeson.stringify(x).then(function (tson) {
console.log(tson);
var back = typeson.parse(tson);
assert(back === 5, "Should allow async to be forced even without async return values");
});
}, function shouldWorkWithPromiseUtilities () {
function makePromises () {
var x = new Typeson.Promise(function (res) {
setTimeout(function () {
res(30);
}, 50);
});
var y = Typeson.Promise.resolve(400);
return [x, y];
}
return new Promise(function (testRes, testRej) {
Typeson.Promise.all(makePromises()).then(function (results) {
assert(results[0] === 30 && results[1] === 400, "Should work with Promise.all");
}).then(function () {
return Typeson.Promise.race(makePromises()).then(function (results) {
assert(results === 400, "Should work with Promise.race");
testRes();
})
});
});
}, function shouldProperlyHandlePromiseExceptions () {
function makeRejectedPromises () {
var x = new Typeson.Promise(function (res, rej) {
setTimeout(function () {
rej(30);
}, 50);
});
var y = new Typeson.Promise(function (res, rej) {
setTimeout(function () {
res(500);
}, 500);
});
return [x, y];
}
return new Promise(function (testRes, testRej) {
makeRejectedPromises()[0].then(null, function (errCode) {
assert(errCode === 30, "`Typeson.Promise` should work with `then(null, onRejected)`");
return Typeson.Promise.reject(400);
}).catch(function (errCode) {
assert(errCode === 400, "`Typeson.Promise` should work with `catch`");
return Typeson.Promise.all(makeRejectedPromises());
}).catch(function (errCode) {
assert(errCode === 30, "Promise.all should work with rejected promises");
return Typeson.Promise.race(makeRejectedPromises());
}).catch(function (errCode) {
assert(errCode === 30, "Promise.race should work with rejected promises");
return new Typeson.Promise(function () {
throw new Error('Sync throw');
});
}).catch(function (err) {
assert(err.message === 'Sync throw', "Typeson.Promise should work with synchronous throws");
return Typeson.Promise.resolve(55);
}).then(null, function () {
throw new Error('Should not reach here');
}).then(function (res) {
assert(res === 55, "Typeson.Promises should bypass `then` without `onResolved`");
return Typeson.Promise.reject(33);
}).then(function () {
throw new Error('Should not reach here');
}).catch(function (errCode) {
assert(errCode === 33, "Typeson.Promises should bypass `then` when rejecting");
testRes();
});
});
}, function asyncREADMEExample () {
function MyAsync (prop) {
this.prop = prop;
}
var typeson = new Typeson({sync: false}).register({
myAsyncType: [
function (x) { return x instanceof MyAsync;},
function (o) {
return new Typeson.Promise(function (resolve, reject) {
setTimeout(function () { // Do something more useful in real code
resolve(o.prop);
}, 800);
});
},
function (data) {
return new MyAsync(data);
}
]
});
var mya = new MyAsync(500);
return typeson.stringify(mya).then(function (result) {
var back = typeson.parse(result, null, {sync: true});
assert(back.prop === 500, "Example of MyAsync should work"); // 500
});
}, function shouldWorkWithAsyncStringify () {
function MyAsync (prop) {
this.prop = prop;
}
var typeson = new Typeson().register({
myAsyncType: [
function (x) { return x instanceof MyAsync;},
function (o) {
return new Typeson.Promise(function (resolve, reject) {
setTimeout(function () { // Do something more useful in real code
resolve(o.prop);
}, 800);
});
},
function (data) {
return new MyAsync(data);
}
]
});
var mya = new MyAsync(500);
return typeson.stringifyAsync(mya).then(function (result) {
var back = typeson.parse(result);
assert(back.prop === 500, "Example of MyAsync should work"); // 500
return typeson.stringifyAsync({prop: 5}, null, null, {throwOnBadSyncType: false});
}).then(function (result) {
var back = typeson.parse(result);
assert(back.prop === 5, "Example of synchronously-resolved simple object should work with async API");
});
}, function shouldWorkWithAsyncEncapsulate () {
function MyAsync (prop) {
this.prop = prop;
}
var typeson = new Typeson().register({
myAsyncType: [
function (x) { return x instanceof MyAsync;},
function (o) {
return new Typeson.Promise(function (resolve, reject) {
setTimeout(function () { // Do something more useful in real code
resolve(o.prop);
}, 800);
});
},
function (data) {
return new MyAsync(data);
}
]
});
var mya = new MyAsync(500);
return typeson.encapsulateAsync(mya).then(function (result) {
assert(result.$ === 500 && result.$types.$[''] === 'myAsyncType', "Example of MyAsync should work");
return typeson.encapsulateAsync({prop: 5}, null, {throwOnBadSyncType: false});
}).then(function (result) {
assert(result.prop === 5, "Example of synchronously-resolved simple object should work with async API");
});
}]);
var keys = Object.keys,
isArray = Array.isArray;
isArray = Array.isArray,
toString = ({}.toString),
getProto = Object.getPrototypeOf,
hasOwn = ({}.hasOwnProperty),
fnToString = hasOwn.toString;
function isThenable (v, catchCheck) {
return Typeson.isObject(v) && typeof v.then === 'function' && (!catchCheck || typeof v.catch === 'function');
}
function toStringTag (val) {
return toString.call(val).slice(8, -1);
}
function hasConstructorOf (a, b) {
if (!a || typeof a !== 'object') {
return false;
}
var proto = getProto(a);
if (!proto) {
return false;
}
var Ctor = hasOwn.call(proto, 'constructor') && proto.constructor;
if (typeof Ctor !== 'function') {
return b === null;
}
return typeof Ctor === 'function' && b !== null && fnToString.call(Ctor) === fnToString.call(b);
}
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);
}
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);
}
function isObject (v) {
return v && typeof v === 'object'
}
/* Typeson - JSON with types

@@ -17,5 +73,6 @@ * License: The MIT License (MIT)

function Typeson (options) {
// Replacers signature: replace (value). Returns falsy if not replacing. Otherwise ["Date", value.getTime()]
var replacers = [];
// Revivers: map {type => reviver}. Sample: {"Date": value => new Date(value)}
// Replacers signature: replace (value). Returns falsy if not replacing. Otherwise ['Date', value.getTime()]
var plainObjectReplacers = [];
var nonplainObjectReplacers = [];
// Revivers: map {type => reviver}. Sample: {'Date': value => new Date(value)}
var revivers = {};

@@ -26,10 +83,25 @@

/** Seraialize given object to Typeson.
/** Serialize given object to Typeson.
*
* Arguments works identical to those of JSON.stringify().
*/
this.stringify = function (obj, replacer, space) { // replacer here has nothing to do with our replacers.
return JSON.stringify (encapsulate(obj), replacer, space);
var stringify = this.stringify = function (obj, replacer, space, opts) { // replacer here has nothing to do with our replacers.
opts = Object.assign({}, options, opts, {stringification: true});
var encapsulated = encapsulate(obj, null, opts);
if (isArray(encapsulated)) {
return JSON.stringify(encapsulated[0], replacer, space);
}
return encapsulated.then(function (res) {
return JSON.stringify(res, replacer, space);
});
};
// Also sync but throws on non-sync result
this.stringifySync = function (obj, replacer, space, opts) {
return stringify(obj, replacer, space, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: true}));
};
this.stringifyAsync = function (obj, replacer, space, opts) {
return stringify(obj, replacer, space, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: false}));
};
/** Parse Typeson back into an obejct.

@@ -39,7 +111,16 @@ *

*/
this.parse = function (text, reviver) {
return revive (JSON.parse (text, reviver)); // This reviver has nothing to do with our revivers.
var parse = this.parse = function (text, reviver, opts) {
opts = Object.assign({}, options, opts, {parse: true});
return revive(JSON.parse(text, reviver), opts); // This reviver has nothing to do with our revivers.
};
/** Encapsulate a complex object into a plain Object by replacing regisered types with
// Also sync but throws on non-sync result
this.parseSync = function (text, reviver, opts) {
return parse(text, reviver, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: true})); // This reviver has nothing to do with our revivers.
};
this.parseAsync = function (text, reviver, opts) {
return parse(text, reviver, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: false})); // This reviver has nothing to do with our revivers.
};
/** Encapsulate a complex object into a plain Object by replacing registered types with
* plain objects representing the types data.

@@ -50,26 +131,112 @@ *

*/
var encapsulate = this.encapsulate = function (obj, stateObj) {
var encapsulate = this.encapsulate = function (obj, stateObj, opts) {
opts = Object.assign({sync: true}, options, opts);
var sync = opts.sync;
var types = {},
refObjs=[], // For checking cyclic references
refKeys=[]; // For checking cyclic references
refObjs = [], // For checking cyclic references
refKeys = [], // For checking cyclic references
promisesDataRoot = [];
// Clone the object deeply while at the same time replacing any special types or cyclic reference:
var cyclic = options && ('cyclic' in options) ? options.cyclic : true;
var ret = _encapsulate ('', obj, cyclic, stateObj || {});
// Add $types to result only if we ever bumped into a special type
if (keys(types).length) {
// Special if array was serialized because JSON would ignore custom $types prop on an array.
if (ret.constructor !== Object || ret.$types) return {$:ret, $types: {$: types}};
ret.$types = types;
var cyclic = opts && ('cyclic' in opts) ? opts.cyclic : true;
var encapsulateObserver = opts.encapsulateObserver;
var ret = _encapsulate('', obj, cyclic, stateObj || {}, promisesDataRoot);
function finish (ret) {
// Add $types to result only if we ever bumped into a special type (or special case where object has own `$types`)
if (keys(types).length) {
if (!ret || !isPlainObject(ret) || // Special if array (or a primitive) was serialized because JSON would ignore custom `$types` prop on it
ret.hasOwnProperty('$types') // Also need to handle if this is an object with its own `$types` property (to avoid ambiguity)
) ret = {$: ret, $types: {$: types}};
else ret.$types = types;
} else if (isObject(ret) && ret.hasOwnProperty('$types')) {
ret = {$: ret, $types: true};
}
return ret;
}
return ret;
function checkPromises (ret, promisesData) {
return Promise.all(
promisesData.map(function (pd) {return pd[1].p;})
).then(function (promResults) {
return Promise.all(
promResults.map(function (promResult) {
var newPromisesData = [];
var prData = promisesData.splice(0, 1)[0];
// var [keypath, , cyclic, stateObj, parentObj, key] = prData;
var keyPath = prData[0];
var cyclic = prData[2];
var stateObj = prData[3];
var parentObj = prData[4];
var key = prData[5];
var detectedType = prData[6];
function _encapsulate (keypath, value, cyclic, stateObj) {
var encaps = _encapsulate(keyPath, promResult, cyclic, stateObj, newPromisesData, true, detectedType);
var isTypesonPromise = hasConstructorOf(encaps, TypesonPromise);
if (keyPath && isTypesonPromise) { // Handle case where an embedded custom type itself returns a `Typeson.Promise`
return encaps.p.then(function (encaps2) {
parentObj[key] = encaps2;
return checkPromises(ret, newPromisesData);
});
}
if (keyPath) parentObj[key] = encaps;
else if (isTypesonPromise) { ret = encaps.p; }
else ret = encaps; // If this is itself a `Typeson.Promise` (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)
return checkPromises(ret, newPromisesData);
})
);
}).then(function () {
return ret;
});
};
return promisesDataRoot.length
? sync && opts.throwOnBadSyncType
? (function () {
throw new TypeError("Sync method requested but async result obtained");
}())
: Promise.resolve(checkPromises(ret, promisesDataRoot)).then(finish)
: !sync && opts.throwOnBadSyncType
? (function () {
throw new TypeError("Async method requested but sync result obtained");
}())
: (opts.stringification && sync // 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
? [finish(ret)]
: (sync
? finish(ret)
: Promise.resolve(finish(ret))
));
function _encapsulate (keypath, value, cyclic, stateObj, promisesData, resolvingTypesonPromise, detectedType) {
var ret, observerData = {};
var runObserver = encapsulateObserver ? function (obj) {
if (!encapsulateObserver) {
return;
}
var type = detectedType || stateObj.type;
encapsulateObserver(Object.assign(obj || observerData, {
keypath: keypath,
value: value,
cyclic: cyclic,
stateObj: stateObj,
promisesData: promisesData,
resolvingTypesonPromise: resolvingTypesonPromise,
awaitingTypesonPromise: hasConstructorOf(value, TypesonPromise)
}, type !== undefined ? {type: type} : {}));
} : null;
var $typeof = typeof value;
if ($typeof in {string:1, boolean:1, number:1, undefined:1 })
return ($typeof === 'undefined' && value === undefined) || ($typeof === 'number' &&
(isNaN(value) || value === -Infinity || value === Infinity)) ?
replace(keypath, value, stateObj) :
value;
if (value == null) return value;
if (cyclic) {
if ($typeof in {string: 1, boolean: 1, number: 1, undefined: 1 }) {
if (value === undefined || ($typeof === 'number' &&
(isNaN(value) || value === -Infinity || value === Infinity))) {
ret = replace(keypath, value, stateObj, promisesData, false, resolvingTypesonPromise);
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) {
// Options set to detect cyclic references and be able to rewrite them.

@@ -83,30 +250,76 @@ var refIndex = refObjs.indexOf(value);

} else {
types[keypath] = "#";
return '#'+refKeys[refIndex];
types[keypath] = '#';
if (runObserver) runObserver({
cyclicKeypath: refKeys[refIndex]
});
return '#' + refKeys[refIndex];
}
}
var replaced = value.constructor === Object ?
value : // Optimization: if plain object, don't try finding a replacer
replace(keypath, value, stateObj);
if (replaced !== value) return replaced;
var isPlainObj = isPlainObject(value);
var isArr = isArray(value);
var replaced = (
((isPlainObj || isArr) && (!plainObjectReplacers.length || stateObj.replaced)) ||
stateObj.iterateIn // Running replace will cause infinite loop as will test positive again
)
// Optimization: if plain object and no plain-object replacers, don't try finding a replacer
? value
: replace(keypath, value, stateObj, promisesData, isPlainObj || isArr);
var clone;
var isArr = value.constructor === Array;
if (value.constructor === Object)
clone = {};
else if (isArr)
clone = new Array(value.length);
else return value; // Only clone vanilla objects and arrays.
if (replaced !== value) {
ret = replaced;
observerData = {replaced: replaced};
} else {
if (isArr || stateObj.iterateIn === 'array') {
clone = new Array(value.length);
observerData = {clone: clone};
} else if (isPlainObj || stateObj.iterateIn === 'object') {
clone = {};
observerData = {clone: clone};
} else if (keypath === '' && hasConstructorOf(value, TypesonPromise)) {
promisesData.push([keypath, value, cyclic, stateObj, undefined, undefined, stateObj.type]);
ret = value;
} else {
ret = value; // Only clone vanilla objects and arrays
}
}
if (runObserver) runObserver();
if (!clone) {
return ret;
}
// Iterate object or array
keys(value).forEach(function (key) {
var val = _encapsulate(keypath + (keypath ? '.':'') + key, value[key], cyclic, {ownKeys: true});
if (val !== undefined) clone[key] = val;
});
// Iterate array for non-own properties (we can't replace the prior loop though as it iterates non-integer keys)
if (isArr) {
if (stateObj.iterateIn) {
for (var key in value) {
var ownKeysObj = {ownKeys: value.hasOwnProperty(key)};
var kp = keypath + (keypath ? '.' : '') + escapeKeyPathComponent(key);
var val = _encapsulate(kp, value[key], !!cyclic, ownKeysObj, promisesData, resolvingTypesonPromise);
if (hasConstructorOf(val, TypesonPromise)) {
promisesData.push([kp, val, !!cyclic, ownKeysObj, clone, key, ownKeysObj.type]);
} else if (val !== undefined) clone[key] = val;
}
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};
var val = _encapsulate(kp, value[key], !!cyclic, ownKeysObj, promisesData, resolvingTypesonPromise);
if (hasConstructorOf(val, TypesonPromise)) {
promisesData.push([kp, val, !!cyclic, ownKeysObj, clone, key, ownKeysObj.type]);
} else if (val !== undefined) 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) {
for (var i = 0, vl = value.length; i < vl; i++) {
if (!(i in value)) {
var val = _encapsulate(keypath + (keypath ? '.':'') + i, value[i], cyclic, {ownKeys: false});
if (val !== undefined) clone[i] = val;
var kp = keypath + (keypath ? '.' : '') + i; // No need to escape numeric
var ownKeysObj = {ownKeys: false};
var val = _encapsulate(kp, undefined, !!cyclic, ownKeysObj, promisesData, resolvingTypesonPromise);
if (hasConstructorOf(val, TypesonPromise)) {
promisesData.push([kp, val, !!cyclic, ownKeysObj, clone, i, ownKeysObj.type]);
} else if (val !== undefined) clone[i] = val;
}
}
if (runObserver) runObserver({endIterateUnsetNumeric: true, end: true});
}

@@ -116,8 +329,10 @@ return clone;

function replace (key, value, stateObj) {
function replace (keypath, value, stateObj, promisesData, plainObject, resolvingTypesonPromise) {
// Encapsulate registered types
var replacers = plainObject ? plainObjectReplacers : nonplainObjectReplacers;
var i = replacers.length;
while (i--) {
if (replacers[i].test(value, stateObj)) {
var type = replacers[i].type;
var replacer = replacers[i];
if (replacer.test(value, stateObj)) {
var type = replacer.type;
if (revivers[type]) {

@@ -128,8 +343,14 @@ // Record the type only if a corresponding reviver exists.

// replacing a type to its equivalent without the need to revive it.
var existing = types[key];
var existing = types[keypath];
// type can comprise an array of types (see test shouldSupportIntermediateTypes)
types[key] = existing ? [type].concat(existing) : type;
types[keypath] = existing ? [type].concat(existing) : type;
}
// Now, also traverse the result in case it contains it own types to replace
return _encapsulate(key, replacers[i].replace(value, stateObj), cyclic && "readonly", stateObj);
// Now, also traverse the result in case it contains its own types to replace
stateObj = Object.assign(stateObj, {replaced: true, type: type});
if ((sync || !replacer.replaceAsync) && !replacer.replace) {
return _encapsulate(keypath, value, cyclic && 'readonly', stateObj, promisesData, resolvingTypesonPromise, type);
}
var replaceMethod = sync || !replacer.replaceAsync ? 'replace' : 'replaceAsync';
return _encapsulate(keypath, replacer[replaceMethod](value, stateObj), cyclic && 'readonly', stateObj, promisesData, resolvingTypesonPromise, type);
}

@@ -141,2 +362,10 @@ }

// Also sync but throws on non-sync result
this.encapsulateSync = function (obj, stateObj, opts) {
return encapsulate(obj, stateObj, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: true}));
};
this.encapsulateAsync = function (obj, stateObj, opts) {
return encapsulate(obj, stateObj, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: false}));
};
/** Revive an encapsulated object.

@@ -147,8 +376,11 @@ * This method is used internally by JSON.parse().

*/
var revive = this.revive = function (obj) {
var types = obj.$types,
var revive = this.revive = function (obj, opts) {
opts = Object.assign({sync: true}, options, opts);
var sync = opts.sync;
var types = obj && obj.$types,
ignore$Types = true;
if (!types) return obj; // No type info added. Revival not needed.
if (types.$ && types.$.constructor === Object) {
// Special when root object is not a trivial Object, it will be encapsulated in $.
if (types === true) return obj.$; // Object happened to have own `$types` property but with no actual types, so we unescape and return that object
if (types.$ && isPlainObject(types.$)) {
// 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
obj = obj.$;

@@ -158,23 +390,63 @@ types = types.$;

}
return _revive ('', obj);
var keyPathResolutions = [];
var ret = _revive('', obj, null, opts);
ret = hasConstructorOf(ret, Undefined) ? undefined : ret;
return isThenable(ret)
? sync && opts.throwOnBadSyncType
? (function () {
throw new TypeError("Sync method requested but async result obtained");
}())
: ret
: !sync && opts.throwOnBadSyncType
? (function () {
throw new TypeError("Async method requested but sync result obtained");
}())
: sync
? ret
: Promise.resolve(ret);
function _revive (keypath, value, target) {
function _revive (keypath, value, target, opts, clone, key) {
if (ignore$Types && keypath === '$types') return;
var type = types[keypath];
if (value && (value.constructor === Object || value.constructor === Array)) {
if (isArray(value) || isPlainObject(value)) {
var clone = isArray(value) ? new Array(value.length) : {};
// Iterate object or array
keys(value).forEach(function (key) {
var val = _revive(keypath + (keypath ? '.':'') + key, value[key], target || clone);
if (val instanceof Undefined) clone[key] = undefined;
var val = _revive(keypath + (keypath ? '.' : '') + escapeKeyPathComponent(key), value[key], target || clone, opts, clone, key);
if (hasConstructorOf(val, Undefined)) clone[key] = undefined;
else if (val !== undefined) clone[key] = val;
});
value = clone;
while (keyPathResolutions.length) { // Try to resolve cyclic reference as soon as available
var kpr = keyPathResolutions[0];
var target = kpr[0];
var keyPath = kpr[1];
var clone = kpr[2];
var key = kpr[3];
var val = getByKeyPath(target, keyPath);
if (hasConstructorOf(val, Undefined)) clone[key] = undefined;
else if (val !== undefined) clone[key] = val;
else break;
keyPathResolutions.splice(0, 1);
}
}
if (!type) return value;
if (type === '#') return getByKeyPath(target, value.substr(1));
return [].concat(type).reduce(function(val, type) {
if (type === '#') {
var ret = getByKeyPath(target, value.substr(1));
if (ret === undefined) { // Cyclic reference not yet available
keyPathResolutions.push([target, value.substr(1), clone, key]);
}
return ret;
}
var sync = opts.sync;
return [].concat(type).reduce(function (val, type) {
var reviver = revivers[type];
if (!reviver) throw new Error ("Unregistered type: " + type);
return reviver(val);
if (!reviver) throw new Error ('Unregistered type: ' + type);
return reviver[
sync && reviver.revive
? 'revive'
: !sync && reviver.reviveAsync
? 'reviveAsync'
: 'revive'
](val);
}, value);

@@ -184,2 +456,10 @@ }

// Also sync but throws on non-sync result
this.reviveSync = function (obj, opts) {
return revive(obj, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: true}));
};
this.reviveAsync = function (obj, opts) {
return revive(obj, Object.assign({}, {throwOnBadSyncType: true}, opts, {sync: false}));
};
/** Register types.

@@ -189,8 +469,13 @@ * For examples how to use this method, see https://github.com/dfahlander/typeson-registry/tree/master/types

*/
this.register = function (typeSpecSets) {
this.register = function (typeSpecSets, opts) {
opts = opts || {};
[].concat(typeSpecSets).forEach(function R (typeSpec) {
if (isArray(typeSpec)) return typeSpec.map(R); // Allow arrays of arrays of arrays...
typeSpec && keys(typeSpec).forEach(function (typeId) {
var spec = typeSpec[typeId],
existingReplacer = replacers.filter(function(r){ return r.type === typeId; });
if (typeId === '#') {
throw new TypeError('# cannot be used as a type name as it is reserved for cyclic objects');
}
var spec = typeSpec[typeId];
var replacers = spec.testPlainObjects ? plainObjectReplacers : nonplainObjectReplacers;
var existingReplacer = replacers.filter(function(r){ return r.type === typeId; });
if (existingReplacer.length) {

@@ -206,14 +491,38 @@ // Remove existing spec and replace with this one.

var Class = spec;
spec = [
function(x){return x.constructor === Class;},
function(x){return assign({}, x)},
function(x){return assign(Object.create(Class.prototype), x)}
];
spec = {
test: function (x) { return x && x.constructor === Class; },
replace: function (x) { return assign({}, x); },
revive: function (x) { return assign(Object.create(Class.prototype), x); }
};
} else if (isArray(spec)) {
spec = {
test: spec[0],
replace: spec[1],
revive: spec[2]
};
}
replacers.push({
var replacerObj = {
type: typeId,
test: spec[0],
replace: spec[1]
});
if (spec[2]) revivers[typeId] = spec[2];
test: spec.test.bind(spec)
};
if (spec.replace) {
replacerObj.replace = spec.replace.bind(spec);
}
if (spec.replaceAsync) {
replacerObj.replaceAsync = spec.replaceAsync.bind(spec);
}
var start = typeof opts.fallback === 'number' ? opts.fallback : (opts.fallback ? 0 : Infinity);
if (spec.testPlainObjects) {
plainObjectReplacers.splice(start, 0, replacerObj);
} else {
nonplainObjectReplacers.splice(start, 0, replacerObj);
}
// Todo: We might consider a testAsync type
if (spec.revive || spec.reviveAsync) {
var reviverObj = {};
if (spec.revive) reviverObj.revive = spec.revive.bind(spec);
if (spec.reviveAsync) reviverObj.reviveAsync = spec.reviveAsync.bind(spec);
revivers[typeId] = reviverObj;
}
regTypes[typeId] = spec; // Record to be retrieved via public types property.

@@ -228,6 +537,16 @@ }

function assign(t,s) {
keys(s).map(function(k){t[k]=s[k];});
keys(s).map(function (k) { t[k] = s[k]; });
return t;
}
/** escapeKeyPathComponent() utility */
function escapeKeyPathComponent (keyPathComponent) {
return keyPathComponent.replace(/~/g, '~0').replace(/\./g, '~1');
}
/** unescapeKeyPathComponent() utility */
function unescapeKeyPathComponent (keyPathComponent) {
return keyPathComponent.replace(/~1/g, '.').replace(/~0/g, '~');
}
/** getByKeyPath() utility */

@@ -237,12 +556,64 @@ function getByKeyPath (obj, keyPath) {

var period = keyPath.indexOf('.');
if (period !== -1) {
var innerObj = obj[keyPath.substr(0, period)];
if (period > -1) {
var innerObj = obj[unescapeKeyPathComponent(keyPath.substr(0, period))];
return innerObj === undefined ? undefined : getByKeyPath(innerObj, keyPath.substr(period + 1));
}
return obj[keyPath];
return obj[unescapeKeyPathComponent(keyPath)];
}
function Undefined () {}
Typeson.Undefined = Undefined;
// With ES6 classes, we may be able to simply use `class TypesonPromise extends Promise` and add a string tag for detection
function TypesonPromise (f) {
this.p = new Promise(f);
};
TypesonPromise.prototype.then = function (onFulfilled, onRejected) {
var that = this;
return new TypesonPromise(function (typesonResolve, typesonReject) {
that.p.then(function (res) {
typesonResolve(onFulfilled ? onFulfilled(res) : res);
}, function (r) {
that.p['catch'](function (res) {
return onRejected ? onRejected(res) : Promise.reject(res);
}).then(typesonResolve, typesonReject);
});
});
};
TypesonPromise.prototype['catch'] = function (onRejected) {
return this.then(null, onRejected);
};
TypesonPromise.resolve = function (v) {
return new TypesonPromise(function (typesonResolve) {
typesonResolve(v);
});
};
TypesonPromise.reject = function (v) {
return new TypesonPromise(function (typesonResolve, typesonReject) {
typesonReject(v);
});
};
['all', 'race'].map(function (meth) {
TypesonPromise[meth] = function (promArr) {
return new TypesonPromise(function (typesonResolve, typesonReject) {
Promise[meth](promArr.map(function (prom) {return prom.p;})).then(typesonResolve, typesonReject);
});
};
});
// The following provide classes meant to avoid clashes with other values
Typeson.Undefined = Undefined; // To insist `undefined` should be added
Typeson.Promise = TypesonPromise; // To support async encapsulation/stringification
// Some fundamental type-checking utilities
Typeson.isThenable = isThenable;
Typeson.toStringTag = toStringTag;
Typeson.hasConstructorOf = hasConstructorOf;
Typeson.isObject = isObject;
Typeson.isPlainObject = isPlainObject;
Typeson.isUserObject = isUserObject;
Typeson.escapeKeyPathComponent = escapeKeyPathComponent;
Typeson.unescapeKeyPathComponent = unescapeKeyPathComponent;
Typeson.getByKeyPath = getByKeyPath;
module.exports = Typeson;
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc