@ungap/structured-clone
Advanced tools
Comparing version 0.2.3 to 0.3.0
'use strict'; | ||
const {deserialize} = require('./deserialize.js'); | ||
const {serialize} = require('./serialize.js'); | ||
const dflt = {transfer: []}; | ||
@@ -19,5 +18,5 @@ /** | ||
structuredClone : | ||
(any, options = dflt) => deserialize(serialize(any, options)); | ||
(any, options) => deserialize(serialize(any, options)); | ||
exports.deserialize = deserialize; | ||
exports.serialize = serialize; |
@@ -6,76 +6,140 @@ 'use strict'; | ||
const EMPTY = ''; | ||
const {toString} = {}; | ||
const {keys} = Object; | ||
const _serialize = (value, $, _) => { | ||
const typeOf = value => { | ||
const type = typeof value; | ||
if (type !== 'object' || !value) | ||
return [PRIMITIVE, type]; | ||
const asString = toString.call(value).slice(8, -1); | ||
switch (asString) { | ||
case 'Array': | ||
return [ARRAY, EMPTY]; | ||
case 'Object': | ||
return [OBJECT, EMPTY]; | ||
case 'Date': | ||
return [DATE, EMPTY]; | ||
case 'RegExp': | ||
return [REGEXP, EMPTY]; | ||
case 'Map': | ||
return [MAP, EMPTY]; | ||
case 'Set': | ||
return [SET, EMPTY]; | ||
} | ||
if (asString.includes('Array')) | ||
return [ARRAY, asString]; | ||
if (asString.includes('Error')) | ||
return [ERROR, asString]; | ||
return [OBJECT, asString]; | ||
}; | ||
const shouldSkip = ([TYPE, type]) => ( | ||
TYPE === PRIMITIVE && | ||
(type === 'function' || type === 'symbol') | ||
); | ||
const as = (out, value, $, _) => { | ||
const index = _.push(out) - 1; | ||
$.set(value, index); | ||
return index; | ||
}; | ||
const pair = (lossy, value, $, _) => { | ||
if ($.has(value)) | ||
return $.get(value); | ||
const index = _.push(value) - 1; | ||
$.set(value, index); | ||
let [TYPE, type] = typeOf(value); | ||
switch (TYPE) { | ||
case PRIMITIVE: { | ||
let entry = value; | ||
switch (type) { | ||
case 'bigint': | ||
TYPE = BIGINT; | ||
entry = value.toString(); | ||
break; | ||
case 'function': | ||
case 'symbol': | ||
if (!lossy) | ||
throw new TypeError('unable to serialize ' + type); | ||
entry = null; | ||
break; | ||
} | ||
return as([TYPE, entry], value, $, _); | ||
} | ||
case ARRAY: { | ||
if (type) | ||
return as([type, [...value]], value, $, _); | ||
const as = serialized => { | ||
_[index] = serialized; | ||
return index; | ||
}; | ||
switch (typeof value) { | ||
case 'object': | ||
if (value !== null) { | ||
const type = toString.call(value).slice(8, -1); | ||
const arr = []; | ||
const index = as([TYPE, arr], value, $, _); | ||
for (const entry of value) | ||
arr.push(pair(lossy, entry, $, _)); | ||
return index; | ||
} | ||
case OBJECT: { | ||
if (type) { | ||
switch (type) { | ||
case 'Array': | ||
return as([ARRAY, value.map(entry => _serialize(entry, $, _))]); | ||
case 'Object': { | ||
const entries = []; | ||
for (const key of keys(value)) | ||
entries.push([_serialize(key, $, _), _serialize(value[key], $, _)]); | ||
return as([OBJECT, entries]); | ||
} | ||
case 'Date': | ||
return as([DATE, value.toISOString()]); | ||
case 'RegExp': { | ||
const {source, flags} = value; | ||
return as([REGEXP, {source, flags}]); | ||
} | ||
case 'Map': { | ||
const entries = []; | ||
for (const [key, entry] of value) | ||
entries.push([_serialize(key, $, _), _serialize(entry, $, _)]); | ||
return as([MAP, entries]); | ||
} | ||
case 'Set': { | ||
const values = []; | ||
for (const entry of value) | ||
values.push(_serialize(entry, $, _)); | ||
return as([SET, values]); | ||
} | ||
case 'BigInt': | ||
return as([type, value.toString()], value, $, _); | ||
case 'Boolean': | ||
case 'Number': | ||
case 'String': | ||
return as([type, value.valueOf()]); | ||
case 'BigInt': | ||
return as([type, value.toString()]); | ||
return as([type, value.valueOf()], value, $, _); | ||
} | ||
} | ||
if (type.includes('Array')) | ||
return as([type, [...value]]); | ||
const entries = []; | ||
const index = as([TYPE, entries], value, $, _); | ||
for (const key of keys(value)) { | ||
if (lossy && shouldSkip(typeOf(value[key]))) | ||
continue; | ||
if (type.includes('Error')) { | ||
const {message} = value; | ||
return as([ERROR, {name: type, message}]); | ||
} | ||
entries.push([ | ||
pair(lossy, key, $, _), | ||
pair(lossy, value[key], $, _) | ||
]); | ||
} | ||
return index; | ||
} | ||
case DATE: | ||
return as([TYPE, value.toISOString()], value, $, _); | ||
case REGEXP: { | ||
const {source, flags} = value; | ||
return as([TYPE, {source, flags}], value, $, _); | ||
} | ||
case MAP: { | ||
const entries = []; | ||
const index = as([TYPE, entries], value, $, _); | ||
for (const [key, entry] of value) { | ||
if (lossy && (shouldSkip(typeOf(key)) || shouldSkip(typeOf(entry)))) | ||
continue; | ||
throw new TypeError; | ||
entries.push([ | ||
pair(lossy, key, $, _), | ||
pair(lossy, entry, $, _) | ||
]); | ||
} | ||
case 'boolean': | ||
case 'number': | ||
case 'string': | ||
case 'undefined': | ||
return as([PRIMITIVE, value]); | ||
case 'bigint': | ||
return as([BIGINT, value.toString()]); | ||
default: | ||
throw new TypeError; | ||
return index; | ||
} | ||
case SET: { | ||
const entries = []; | ||
const index = as([TYPE, entries], value, $, _); | ||
for (const entry of value) { | ||
if (lossy && shouldSkip(typeOf(entry))) | ||
continue; | ||
entries.push(pair(lossy, entry, $, _)); | ||
} | ||
return index; | ||
} | ||
} | ||
const {message} = value; | ||
return as([TYPE, {name: type, message}], value, $, _); | ||
}; | ||
@@ -90,8 +154,11 @@ | ||
* @param {any} serializable a serializable value. | ||
* @param {{lossy?: boolean}?} options an object with a `lossy` property that, | ||
* if `true`, will not throw errors on incompatible types, and behave more | ||
* like JSON stringify would behave. Symbol and Function will be discarded. | ||
* @returns {Record[]} | ||
*/ | ||
const serialize = serializable => { | ||
const serialize = (serializable, options = {}) => { | ||
const _ = []; | ||
return _serialize(serializable, new Map, _), _; | ||
return pair(!!options.lossy, serializable, new Map, _), _; | ||
}; | ||
exports.serialize = serialize; |
'use strict'; | ||
let i = 0; | ||
const PRIMITIVE = i++; | ||
const PRIMITIVE = 0; | ||
exports.PRIMITIVE = PRIMITIVE; | ||
const ARRAY = i++; | ||
const ARRAY = 1; | ||
exports.ARRAY = ARRAY; | ||
const OBJECT = i++; | ||
const OBJECT = 2; | ||
exports.OBJECT = OBJECT; | ||
const DATE = i++; | ||
const DATE = 3; | ||
exports.DATE = DATE; | ||
const REGEXP = i++; | ||
const REGEXP = 4; | ||
exports.REGEXP = REGEXP; | ||
const MAP = i++; | ||
const MAP = 5; | ||
exports.MAP = MAP; | ||
const SET = i++; | ||
const SET = 6; | ||
exports.SET = SET; | ||
const ERROR = i++; | ||
const ERROR = 7; | ||
exports.ERROR = ERROR; | ||
const BIGINT = i++; | ||
const BIGINT = 8; | ||
exports.BIGINT = BIGINT; | ||
// export const SYMBOL = 9; |
import {deserialize} from './deserialize.js'; | ||
import {serialize} from './serialize.js'; | ||
const dflt = {transfer: []}; | ||
@@ -18,4 +17,4 @@ /** | ||
structuredClone : | ||
(any, options = dflt) => deserialize(serialize(any, options)); | ||
(any, options) => deserialize(serialize(any, options)); | ||
export {deserialize, serialize}; |
@@ -7,76 +7,140 @@ import { | ||
const EMPTY = ''; | ||
const {toString} = {}; | ||
const {keys} = Object; | ||
const _serialize = (value, $, _) => { | ||
const typeOf = value => { | ||
const type = typeof value; | ||
if (type !== 'object' || !value) | ||
return [PRIMITIVE, type]; | ||
const asString = toString.call(value).slice(8, -1); | ||
switch (asString) { | ||
case 'Array': | ||
return [ARRAY, EMPTY]; | ||
case 'Object': | ||
return [OBJECT, EMPTY]; | ||
case 'Date': | ||
return [DATE, EMPTY]; | ||
case 'RegExp': | ||
return [REGEXP, EMPTY]; | ||
case 'Map': | ||
return [MAP, EMPTY]; | ||
case 'Set': | ||
return [SET, EMPTY]; | ||
} | ||
if (asString.includes('Array')) | ||
return [ARRAY, asString]; | ||
if (asString.includes('Error')) | ||
return [ERROR, asString]; | ||
return [OBJECT, asString]; | ||
}; | ||
const shouldSkip = ([TYPE, type]) => ( | ||
TYPE === PRIMITIVE && | ||
(type === 'function' || type === 'symbol') | ||
); | ||
const as = (out, value, $, _) => { | ||
const index = _.push(out) - 1; | ||
$.set(value, index); | ||
return index; | ||
}; | ||
const pair = (lossy, value, $, _) => { | ||
if ($.has(value)) | ||
return $.get(value); | ||
const index = _.push(value) - 1; | ||
$.set(value, index); | ||
let [TYPE, type] = typeOf(value); | ||
switch (TYPE) { | ||
case PRIMITIVE: { | ||
let entry = value; | ||
switch (type) { | ||
case 'bigint': | ||
TYPE = BIGINT; | ||
entry = value.toString(); | ||
break; | ||
case 'function': | ||
case 'symbol': | ||
if (!lossy) | ||
throw new TypeError('unable to serialize ' + type); | ||
entry = null; | ||
break; | ||
} | ||
return as([TYPE, entry], value, $, _); | ||
} | ||
case ARRAY: { | ||
if (type) | ||
return as([type, [...value]], value, $, _); | ||
const as = serialized => { | ||
_[index] = serialized; | ||
return index; | ||
}; | ||
switch (typeof value) { | ||
case 'object': | ||
if (value !== null) { | ||
const type = toString.call(value).slice(8, -1); | ||
const arr = []; | ||
const index = as([TYPE, arr], value, $, _); | ||
for (const entry of value) | ||
arr.push(pair(lossy, entry, $, _)); | ||
return index; | ||
} | ||
case OBJECT: { | ||
if (type) { | ||
switch (type) { | ||
case 'Array': | ||
return as([ARRAY, value.map(entry => _serialize(entry, $, _))]); | ||
case 'Object': { | ||
const entries = []; | ||
for (const key of keys(value)) | ||
entries.push([_serialize(key, $, _), _serialize(value[key], $, _)]); | ||
return as([OBJECT, entries]); | ||
} | ||
case 'Date': | ||
return as([DATE, value.toISOString()]); | ||
case 'RegExp': { | ||
const {source, flags} = value; | ||
return as([REGEXP, {source, flags}]); | ||
} | ||
case 'Map': { | ||
const entries = []; | ||
for (const [key, entry] of value) | ||
entries.push([_serialize(key, $, _), _serialize(entry, $, _)]); | ||
return as([MAP, entries]); | ||
} | ||
case 'Set': { | ||
const values = []; | ||
for (const entry of value) | ||
values.push(_serialize(entry, $, _)); | ||
return as([SET, values]); | ||
} | ||
case 'BigInt': | ||
return as([type, value.toString()], value, $, _); | ||
case 'Boolean': | ||
case 'Number': | ||
case 'String': | ||
return as([type, value.valueOf()]); | ||
case 'BigInt': | ||
return as([type, value.toString()]); | ||
return as([type, value.valueOf()], value, $, _); | ||
} | ||
} | ||
if (type.includes('Array')) | ||
return as([type, [...value]]); | ||
const entries = []; | ||
const index = as([TYPE, entries], value, $, _); | ||
for (const key of keys(value)) { | ||
if (lossy && shouldSkip(typeOf(value[key]))) | ||
continue; | ||
if (type.includes('Error')) { | ||
const {message} = value; | ||
return as([ERROR, {name: type, message}]); | ||
} | ||
entries.push([ | ||
pair(lossy, key, $, _), | ||
pair(lossy, value[key], $, _) | ||
]); | ||
} | ||
return index; | ||
} | ||
case DATE: | ||
return as([TYPE, value.toISOString()], value, $, _); | ||
case REGEXP: { | ||
const {source, flags} = value; | ||
return as([TYPE, {source, flags}], value, $, _); | ||
} | ||
case MAP: { | ||
const entries = []; | ||
const index = as([TYPE, entries], value, $, _); | ||
for (const [key, entry] of value) { | ||
if (lossy && (shouldSkip(typeOf(key)) || shouldSkip(typeOf(entry)))) | ||
continue; | ||
throw new TypeError; | ||
entries.push([ | ||
pair(lossy, key, $, _), | ||
pair(lossy, entry, $, _) | ||
]); | ||
} | ||
case 'boolean': | ||
case 'number': | ||
case 'string': | ||
case 'undefined': | ||
return as([PRIMITIVE, value]); | ||
case 'bigint': | ||
return as([BIGINT, value.toString()]); | ||
default: | ||
throw new TypeError; | ||
return index; | ||
} | ||
case SET: { | ||
const entries = []; | ||
const index = as([TYPE, entries], value, $, _); | ||
for (const entry of value) { | ||
if (lossy && shouldSkip(typeOf(entry))) | ||
continue; | ||
entries.push(pair(lossy, entry, $, _)); | ||
} | ||
return index; | ||
} | ||
} | ||
const {message} = value; | ||
return as([TYPE, {name: type, message}], value, $, _); | ||
}; | ||
@@ -91,7 +155,10 @@ | ||
* @param {any} serializable a serializable value. | ||
* @param {{lossy?: boolean}?} options an object with a `lossy` property that, | ||
* if `true`, will not throw errors on incompatible types, and behave more | ||
* like JSON stringify would behave. Symbol and Function will be discarded. | ||
* @returns {Record[]} | ||
*/ | ||
export const serialize = serializable => { | ||
export const serialize = (serializable, options = {}) => { | ||
const _ = []; | ||
return _serialize(serializable, new Map, _), _; | ||
return pair(!!options.lossy, serializable, new Map, _), _; | ||
}; |
@@ -1,11 +0,10 @@ | ||
let i = 0; | ||
export const PRIMITIVE = i++; | ||
export const ARRAY = i++; | ||
export const OBJECT = i++; | ||
export const DATE = i++; | ||
export const REGEXP = i++; | ||
export const MAP = i++; | ||
export const SET = i++; | ||
export const ERROR = i++; | ||
export const BIGINT = i++; | ||
export const PRIMITIVE = 0; | ||
export const ARRAY = 1; | ||
export const OBJECT = 2; | ||
export const DATE = 3; | ||
export const REGEXP = 4; | ||
export const MAP = 5; | ||
export const SET = 6; | ||
export const ERROR = 7; | ||
export const BIGINT = 8; | ||
// export const SYMBOL = 9; |
{ | ||
"name": "@ungap/structured-clone", | ||
"version": "0.2.3", | ||
"version": "0.3.0", | ||
"description": "A structuredClone polyfill", | ||
@@ -5,0 +5,0 @@ "main": "./cjs/index.js", |
@@ -37,1 +37,23 @@ # structuredClone polyfill | ||
``` | ||
### Extra Features | ||
There is no middle-ground between the structured clone algorithm and JSON: | ||
* JSON is more relaxed about incompatible values: it just ignores these | ||
* Structured clone is inflexible regarding incompatible values, yet it makes specialized instances impossible to reconstruct, plus it doesn't offer any helper, such as `toJSON()`, to make serialization possible, or better, with specific cases | ||
This module specialized `serialize` export offers, within the optional extra argument, a **lossy** property to avoid throwing when incompatible types are found down the road (function, symbol, ...), so that it is possible to send with less worrying about thrown errors. | ||
```js | ||
// as default export | ||
import structuredClone from '@ungap/structured-clone'; | ||
const cloned = structuredClone({ | ||
method() { | ||
// ignored, won't be cloned | ||
}, | ||
special: Symbol('also ignored') | ||
}); | ||
``` | ||
The behavior is the same found in *JSON* when it comes to *Array*, so that unsupported values will result as `null` placeholders instead. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
19444
484
59