diff --git a/cjs/deserialize.js b/cjs/deserialize.js index cb84880..f3f739d 100644 --- a/cjs/deserialize.js +++ b/cjs/deserialize.js @@ -88,8 +88,9 @@ const deserializer = ($, _) => { case 'ArrayBuffer': return as(new Uint8Array(value).buffer, value); case 'DataView': { - const { buffer } = new Uint8Array(value); - return as(new DataView(buffer), value); + const {buffer, byteOffset, byteLength} = Array.isArray(value) ? + {buffer: value, byteOffset: 0, byteLength: value.length} : value; + return as(new DataView(new Uint8Array(buffer).buffer, byteOffset, byteLength), value); } case '-0': return -0; } diff --git a/cjs/serialize.js b/cjs/serialize.js index 45bce6e..57a1b24 100644 --- a/cjs/serialize.js +++ b/cjs/serialize.js @@ -85,7 +85,10 @@ const serializer = (strict, json, $, _) => { if (type) { let spread = value; if (type === 'DataView') { - spread = new Uint8Array(value.buffer); + const {buffer, byteOffset, byteLength} = value; + return as([type, { + buffer: [...new Uint8Array(buffer)], byteOffset, byteLength + }], value); } else if (type === 'ArrayBuffer') { spread = new Uint8Array(value); diff --git a/esm/deserialize.js b/esm/deserialize.js index 86920f0..5ab43da 100644 --- a/esm/deserialize.js +++ b/esm/deserialize.js @@ -90,8 +90,9 @@ const deserializer = ($, _) => { case 'ArrayBuffer': return as(new Uint8Array(value).buffer, value); case 'DataView': { - const { buffer } = new Uint8Array(value); - return as(new DataView(buffer), value); + const {buffer, byteOffset, byteLength} = Array.isArray(value) ? + {buffer: value, byteOffset: 0, byteLength: value.length} : value; + return as(new DataView(new Uint8Array(buffer).buffer, byteOffset, byteLength), value); } case '-0': return -0; } diff --git a/esm/serialize.js b/esm/serialize.js index 6217735..c103c1f 100644 --- a/esm/serialize.js +++ b/esm/serialize.js @@ -87,7 +87,10 @@ const serializer = (strict, json, $, _) => { if (type) { let spread = value; if (type === 'DataView') { - spread = new Uint8Array(value.buffer); + const {buffer, byteOffset, byteLength} = value; + return as([type, { + buffer: [...new Uint8Array(buffer)], byteOffset, byteLength + }], value); } else if (type === 'ArrayBuffer') { spread = new Uint8Array(value); diff --git a/structured-json.js b/structured-json.js index 4fa6359..8c4c0be 100644 --- a/structured-json.js +++ b/structured-json.js @@ -1 +1 @@ -var StructuredJSON=function(e){"use strict";const{defineProperty:r}=Object,t="object"==typeof self?self:globalThis,n=(e,r)=>{switch(e){case"Function":case"SharedWorker":case"Worker":case"eval":case"setInterval":case"setTimeout":throw new TypeError("unable to deserialize "+e)}return new t[e](r)},s=e=>((e,s)=>{const a=(r,t)=>(e.set(t,r),r),c=o=>{if(e.has(o))return e.get(o);const[u,i]=s[o];switch(u){case 0:case-1:return a(i,o);case 1:{const e=a([],o);for(const r of i)e.push(c(r));return e}case 2:{const e=a({},o);for(const[t,n]of i){const s=c(t),a=c(n);"__proto__"===s?r(e,s,{value:a,configurable:!0,enumerable:!0,writable:!0}):e[s]=a}return e}case 3:return a(new Date(i),o);case 4:{const{source:e,flags:r}=i;return a(new RegExp(e,r),o)}case 5:{const e=a(new Map,o);for(const[r,t]of i)e.set(c(r),c(t));return e}case 6:{const e=a(new Set,o);for(const r of i)e.add(c(r));return e}case 7:{const{name:e,message:r}=i;return a("function"==typeof t[e]?n(e,r):new Error(r),o)}case 8:return a(BigInt(i),o);case"BigInt":return a(Object(BigInt(i)),o);case"ArrayBuffer":return a(new Uint8Array(i).buffer,i);case"DataView":{const{buffer:e}=new Uint8Array(i);return a(new DataView(e),i)}case"-0":return-0}return a(n(u,i),o)};return c})(new Map,e)(0),a="",{toString:c}={},{keys:o,is:u}=Object,i=e=>{const r=typeof e;if("object"!==r||!e)return[0,r];const t=c.call(e).slice(8,-1);switch(t){case"Array":return[1,a];case"Object":return[2,a];case"Date":return[3,a];case"RegExp":return[4,a];case"Map":return[5,a];case"Set":return[6,a];case"DataView":return[1,t]}return t.includes("Array")?[1,t]:e instanceof Error?[7,e.name||"Error"]:[2,t]},f=([e,r])=>0===e&&("function"===r||"symbol"===r),l=(e,{json:r,lossy:t}={})=>{const n=[];return((e,r,t,n)=>{const s=(e,r)=>{const s=n.push(e)-1;return t.set(r,s),s},c=l=>{if(t.has(l))return t.get(l);let[w,g]=i(l);switch(w){case 0:{let r=l;switch(g){case"bigint":w=8,r=l.toString();break;case"number":if(!l&&u(l,-0))return n.push(["-0"])-1;break;case"function":case"symbol":if(e)throw new TypeError("unable to serialize "+g);r=null;break;case"undefined":return s([-1],l)}return s([w,r],l)}case 1:{if(g){let e=l;return"DataView"===g?e=new Uint8Array(l.buffer):"ArrayBuffer"===g&&(e=new Uint8Array(l)),s([g,[...e]],l)}const e=[],r=s([w,e],l);for(const r of l)e.push(c(r));return r}case 2:{if(g)switch(g){case"BigInt":return s([g,l.toString()],l);case"Boolean":case"Number":case"String":return s([g,l.valueOf()],l)}if(r&&"toJSON"in l)return c(l.toJSON());const t=[],n=s([w,t],l);for(const r of o(l))!e&&f(i(l[r]))||t.push([c(r),c(l[r])]);return n}case 3:return s([w,isNaN(l.getTime())?a:l.toISOString()],l);case 4:{const{source:e,flags:r}=l;return s([w,{source:e,flags:r}],l)}case 5:{const r=[],t=s([w,r],l);for(const[t,n]of l)(e||!f(i(t))&&!f(i(n)))&&r.push([c(t),c(n)]);return t}case 6:{const r=[],t=s([w,r],l);for(const t of l)!e&&f(i(t))||r.push(c(t));return t}}const{message:b}=l;return s([w,{name:g,message:b}],l)};return c})(!(r||t),!!r,new Map,n)(e),n},{parse:w,stringify:g}=JSON,b={json:!0,lossy:!0};return e.parse=e=>s(w(e)),e.stringify=e=>g(l(e,b)),e}({}); +var StructuredJSON=function(e){"use strict";const{defineProperty:r}=Object,t="object"==typeof self?self:globalThis,n=(e,r)=>{switch(e){case"Function":case"SharedWorker":case"Worker":case"eval":case"setInterval":case"setTimeout":throw new TypeError("unable to deserialize "+e)}return new t[e](r)},s=e=>((e,s)=>{const a=(r,t)=>(e.set(t,r),r),c=o=>{if(e.has(o))return e.get(o);const[u,f]=s[o];switch(u){case 0:case-1:return a(f,o);case 1:{const e=a([],o);for(const r of f)e.push(c(r));return e}case 2:{const e=a({},o);for(const[t,n]of f){const s=c(t),a=c(n);"__proto__"===s?r(e,s,{value:a,configurable:!0,enumerable:!0,writable:!0}):e[s]=a}return e}case 3:return a(new Date(f),o);case 4:{const{source:e,flags:r}=f;return a(new RegExp(e,r),o)}case 5:{const e=a(new Map,o);for(const[r,t]of f)e.set(c(r),c(t));return e}case 6:{const e=a(new Set,o);for(const r of f)e.add(c(r));return e}case 7:{const{name:e,message:r}=f;return a("function"==typeof t[e]?n(e,r):new Error(r),o)}case 8:return a(BigInt(f),o);case"BigInt":return a(Object(BigInt(f)),o);case"ArrayBuffer":return a(new Uint8Array(f).buffer,f);case"DataView":{const{buffer:e,byteOffset:r,byteLength:t}=Array.isArray(f)?{buffer:f,byteOffset:0,byteLength:f.length}:f;return a(new DataView(new Uint8Array(e).buffer,r,t),f)}case"-0":return-0}return a(n(u,f),o)};return c})(new Map,e)(0),a="",{toString:c}={},{keys:o,is:u}=Object,f=e=>{const r=typeof e;if("object"!==r||!e)return[0,r];const t=c.call(e).slice(8,-1);switch(t){case"Array":return[1,a];case"Object":return[2,a];case"Date":return[3,a];case"RegExp":return[4,a];case"Map":return[5,a];case"Set":return[6,a];case"DataView":return[1,t]}return t.includes("Array")?[1,t]:e instanceof Error?[7,e.name||"Error"]:[2,t]},i=([e,r])=>0===e&&("function"===r||"symbol"===r),b=(e,{json:r,lossy:t}={})=>{const n=[];return((e,r,t,n)=>{const s=(e,r)=>{const s=n.push(e)-1;return t.set(r,s),s},c=b=>{if(t.has(b))return t.get(b);let[l,y]=f(b);switch(l){case 0:{let r=b;switch(y){case"bigint":l=8,r=b.toString();break;case"number":if(!b&&u(b,-0))return n.push(["-0"])-1;break;case"function":case"symbol":if(e)throw new TypeError("unable to serialize "+y);r=null;break;case"undefined":return s([-1],b)}return s([l,r],b)}case 1:{if(y){let e=b;if("DataView"===y){const{buffer:e,byteOffset:r,byteLength:t}=b;return s([y,{buffer:[...new Uint8Array(e)],byteOffset:r,byteLength:t}],b)}return"ArrayBuffer"===y&&(e=new Uint8Array(b)),s([y,[...e]],b)}const e=[],r=s([l,e],b);for(const r of b)e.push(c(r));return r}case 2:{if(y)switch(y){case"BigInt":return s([y,b.toString()],b);case"Boolean":case"Number":case"String":return s([y,b.valueOf()],b)}if(r&&"toJSON"in b)return c(b.toJSON());const t=[],n=s([l,t],b);for(const r of o(b))!e&&i(f(b[r]))||t.push([c(r),c(b[r])]);return n}case 3:return s([l,isNaN(b.getTime())?a:b.toISOString()],b);case 4:{const{source:e,flags:r}=b;return s([l,{source:e,flags:r}],b)}case 5:{const r=[],t=s([l,r],b);for(const[t,n]of b)(e||!i(f(t))&&!i(f(n)))&&r.push([c(t),c(n)]);return t}case 6:{const r=[],t=s([l,r],b);for(const t of b)!e&&i(f(t))||r.push(c(t));return t}}const{message:g}=b;return s([l,{name:y,message:g}],b)};return c})(!(r||t),!!r,new Map,n)(e),n},{parse:l,stringify:y}=JSON,g={json:!0,lossy:!0};return e.parse=e=>s(l(e)),e.stringify=e=>y(b(e,g)),e}({}); diff --git a/test/index.js b/test/index.js index b03edc4..1106223 100644 --- a/test/index.js +++ b/test/index.js @@ -8,6 +8,55 @@ const assert = (expected, result, message = '') => { } }; +{ + const buffer = new Uint8Array([10, 20, 30, 40]).buffer; + for (const [offset, length] of [[0, 2], [1, 2], [2, 2], [3, 0], [4, 0], [0, 0], [0, 4]]) { + const original = new DataView(buffer, offset, length); + const [record] = serialize(original); + assert(record.length, 2, 'keeps the type/value pair contract'); + assert(JSON.stringify(record), JSON.stringify(['DataView', { + buffer: [10, 20, 30, 40], byteOffset: offset, byteLength: length + }]), 'uses named DataView fields for every view'); + const native = typeof globalThis.structuredClone === 'function' ? + globalThis.structuredClone(original) : original; + for (const result of [ + deserialize(serialize(original)), + deserialize(JSON.parse(JSON.stringify(serialize(original)))), + parse(stringify(original)), + structuredClone(original, {lossy: true}) + ]) { + assert(result instanceof DataView, true); + assert(result.byteOffset, native.byteOffset, 'preserves DataView byteOffset'); + assert(result.byteLength, native.byteLength, 'preserves DataView byteLength'); + assert(result.buffer.byteLength, native.buffer.byteLength); + assert([...new Uint8Array(result.buffer)].join(','), [...new Uint8Array(native.buffer)].join(',')); + for (let i = 0; i < length; i++) + assert(result.getUint8(i), native.getUint8(i)); + let outOfBounds = false; + try { + result.getUint8(length); + } + catch (error) { + outOfBounds = error instanceof RangeError; + } + assert(outOfBounds, true, 'keeps reads outside the DataView out of bounds'); + if (length) { + result.setUint8(0, 255); + assert(original.getUint8(0), native.getUint8(0), 'does not share the source buffer'); + } + } + } + for (const bytes of [[], [10, 20, 30, 40]]) { + const legacy = deserialize([['DataView', bytes]]); + assert(legacy.byteOffset, 0); + assert(legacy.byteLength, bytes.length); + assert([...new Uint8Array(legacy.buffer)].join(','), bytes.join(',')); + } + const empty = deserialize(serialize(new DataView(new ArrayBuffer(0)))); + assert(empty.byteOffset, 0); + assert(empty.byteLength, 0); +} + // @see https://github.com/ungap/structured-clone/issues/4 const withUndefined = stringify({foo: 'test', bar: undefined, foobar: null}); assert(withUndefined, '[[2,[[1,2],[3,4],[5,6]]],[0,"foo"],[0,"test"],[0,"bar"],[-1],[0,"foobar"],[0,null]]'); @@ -66,7 +115,7 @@ function test(firstRun = false) { console.timeEnd('serialized in'); assert(JSON.stringify(serialized), [ - `[[2,[[1,2],[3,4],[5,6],[7,8],[9,10],[11,12],[13,14],[15,16],[17,18],[19,20],[21,22],[24,25],[27,28],[29,30],[31,32],[33,34],[35,36],[37,38],[39,40]]],[0,"arr"],[1,[0,0,0]],[0,"bigint"],[8,"1"],[0,"boolean"],[0,true],[0,"number"],[0,123],[0,"string"],[0,""],[0,"undefined"],[-1],[0,"null"],[0,null],[0,"buffer"],["ArrayBuffer",[0,1,2,3,4,5,6,7]],[0,"dataview"],["DataView",[0,1,2,3,4,5,6,7]],[0,"int"],["Uint32Array",[1,2,3]],[0,"map"],[5,[[23,8]]],[0,"a"],[0,"set"],[6,[23,26]],[0,"b"],[0,"Bool"],["Boolean",false],[0,"Num"],["Number",0],[0,"Str"],["String",""],[0,"re"],[4,{"source":"test","flags":"gim"}],[0,"error"],[7,{"name":"Error","message":"test"}],[0,"BI"],["BigInt","1"],[0,"date"],[3,"${date.toISOString()}"]]` + `[[2,[[1,2],[3,4],[5,6],[7,8],[9,10],[11,12],[13,14],[15,16],[17,18],[19,20],[21,22],[24,25],[27,28],[29,30],[31,32],[33,34],[35,36],[37,38],[39,40]]],[0,"arr"],[1,[0,0,0]],[0,"bigint"],[8,"1"],[0,"boolean"],[0,true],[0,"number"],[0,123],[0,"string"],[0,""],[0,"undefined"],[-1],[0,"null"],[0,null],[0,"buffer"],["ArrayBuffer",[0,1,2,3,4,5,6,7]],[0,"dataview"],["DataView",{"buffer":[0,1,2,3,4,5,6,7],"byteOffset":0,"byteLength":8}],[0,"int"],["Uint32Array",[1,2,3]],[0,"map"],[5,[[23,8]]],[0,"a"],[0,"set"],[6,[23,26]],[0,"b"],[0,"Bool"],["Boolean",false],[0,"Num"],["Number",0],[0,"Str"],["String",""],[0,"re"],[4,{"source":"test","flags":"gim"}],[0,"error"],[7,{"name":"Error","message":"test"}],[0,"BI"],["BigInt","1"],[0,"date"],[3,"${date.toISOString()}"]]` ].join(',')); // firstRun && console.log(serialized);