├── .gitignore ├── types ├── views.d.ts ├── options.d.ts ├── index.d.ts ├── decoder.d.ts ├── encoder.d.ts └── globals.d.ts ├── old ├── float16array.js ├── index.js ├── encode │ └── utils.js ├── constants.js ├── number.js ├── decode.js └── encode.js ├── test ├── decode.js ├── index.html ├── encode.js ├── worker │ ├── structured │ │ ├── roundtrip.js │ │ ├── double.js │ │ └── triple.js │ ├── buffered │ │ ├── roundtrip.js │ │ ├── decode.js │ │ ├── serialization.js │ │ ├── triple.js │ │ └── double.js │ ├── json │ │ └── serialization.js │ ├── flatted │ │ └── serialization.js │ ├── msgpack │ │ └── serialization.js │ ├── ungap │ │ ├── decode.js │ │ └── serialization.js │ ├── jspack │ │ ├── decode.js │ │ ├── serialization.js │ │ └── serialization-nc.js │ ├── index.html │ ├── bson │ │ └── serialization.js │ ├── messagepack │ │ └── serialization.js │ ├── benchmark.js │ ├── index.js │ ├── tests.js │ └── carts.json ├── encode.html ├── browser │ ├── index.html │ └── index.js ├── sab │ ├── index.html │ ├── buffered │ │ ├── roundtrip.js │ │ └── double-roundtrip.js │ ├── ungap │ │ └── roundtrip.js │ ├── coincident │ │ └── roundtrip.js │ └── index.js ├── index.js ├── index_old.html ├── benchmark.js ├── worker.html ├── data.js ├── data_old.js ├── cover_old.js └── cover.js ├── asd.js ├── src ├── index.js ├── options.js ├── views.js ├── globals.js ├── decoder.js └── encoder.js ├── .github └── workflows │ └── node.js.yml ├── LICENSE ├── mini-coi.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .clinic/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /types/views.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: any[]; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /old/float16array.js: -------------------------------------------------------------------------------- 1 | export default (globalThis.Float16Array || class Float16Array {}); 2 | -------------------------------------------------------------------------------- /old/index.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import encode from './encode.js'; 4 | import decode from './decode.js'; 5 | 6 | export { encode, decode }; 7 | -------------------------------------------------------------------------------- /test/decode.js: -------------------------------------------------------------------------------- 1 | import { data } from './data.js'; 2 | import { encode, decode } from '../src/index.js'; 3 | 4 | const encoded = encode(data); 5 | 6 | let result; 7 | for (let i = 0; i < 100; i++) result = decode(encoded, { recursion: 'all' }); 8 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/encode.js: -------------------------------------------------------------------------------- 1 | import { data } from './data.js'; 2 | import { encode } from '../src/index.js'; 3 | import carts from './worker/carts.json' with { type: 'json' }; 4 | 5 | let result; 6 | for (let i = 0; i < 100; i++) result = encode(carts, { recursion: 'none' }); 7 | -------------------------------------------------------------------------------- /asd.js: -------------------------------------------------------------------------------- 1 | const bf = new ArrayBuffer(128); 2 | const typed = new Int32Array(bf); 3 | const dataview = new DataView(bf); 4 | 5 | const data = { 6 | bf, 7 | typed, // here it fails 8 | dataview, // here it fails too 9 | }; 10 | 11 | structuredClone(data); 12 | 13 | -------------------------------------------------------------------------------- /types/options.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _default { 2 | let littleEndian: boolean; 3 | let circular: boolean; 4 | let byteOffset: number; 5 | let byteLength: number; 6 | let useFloat32: boolean; 7 | let useUTF16: boolean; 8 | let mirrored: any[]; 9 | } 10 | export default _default; 11 | -------------------------------------------------------------------------------- /test/worker/structured/roundtrip.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 4 | if (ACTION === Benchmark.RUN) { 5 | const [data] = rest; 6 | postMessage([ACTION, data]); 7 | } 8 | else postMessage([Benchmark.INIT]); 9 | }); 10 | -------------------------------------------------------------------------------- /test/worker/buffered/roundtrip.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 4 | if (ACTION === Benchmark.RUN) { 5 | const [data] = rest; 6 | postMessage([ACTION, data], [data.buffer]); 7 | } 8 | else postMessage([Benchmark.INIT]); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import defaultOptions from './options.js'; 4 | import { decoder } from './decoder.js'; 5 | import { encoder } from './encoder.js'; 6 | 7 | export default class JSPack { 8 | constructor(options = defaultOptions) { 9 | this.decode = decoder(options); 10 | this.encode = encoder(options); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/encode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { 4 | DataView, 5 | Uint16Array, 6 | } from './globals.js'; 7 | 8 | export default { 9 | littleEndian: new DataView(new Uint16Array([256]).buffer).getUint16(0, true) === 256, 10 | circular: true, 11 | byteOffset: 0, 12 | byteLength: 0x1000000, 13 | useFloat32: false, 14 | useUTF16: false, 15 | mirrored: [], 16 | }; 17 | -------------------------------------------------------------------------------- /test/worker/json/serialization.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | const { parse, stringify } = JSON; 4 | 5 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 6 | if (ACTION === Benchmark.RUN) { 7 | const [data] = rest; 8 | const json = parse(data); 9 | postMessage([ACTION, stringify(json)]); 10 | } 11 | else postMessage([Benchmark.INIT]); 12 | }); 13 | -------------------------------------------------------------------------------- /test/worker/flatted/serialization.js: -------------------------------------------------------------------------------- 1 | import { parse, stringify } from 'https://esm.run/flatted'; 2 | 3 | import Benchmark from '../benchmark.js'; 4 | 5 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 6 | if (ACTION === Benchmark.RUN) { 7 | const [data] = rest; 8 | const json = parse(data); 9 | postMessage([ACTION, stringify(json)]); 10 | } 11 | else postMessage([Benchmark.INIT]); 12 | }); 13 | -------------------------------------------------------------------------------- /test/worker/buffered/decode.js: -------------------------------------------------------------------------------- 1 | import { decode } from '../../../src/index.js'; 2 | import Benchmark from '../benchmark.js'; 3 | 4 | export let decoded = null; 5 | 6 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 7 | if (ACTION === Benchmark.RUN) { 8 | const [data] = rest; 9 | decoded = decode(data); 10 | postMessage([ACTION, data], [data.buffer]); 11 | } 12 | else postMessage([Benchmark.INIT]); 13 | }); 14 | -------------------------------------------------------------------------------- /test/worker/msgpack/serialization.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | import * as msgpack from 'https://esm.run/@msgpack/msgpack'; 4 | 5 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 6 | if (ACTION === Benchmark.RUN) { 7 | const [data] = rest; 8 | const json = msgpack.decode(data); 9 | postMessage([ACTION, msgpack.encode(json)]); 10 | } 11 | else postMessage([Benchmark.INIT]); 12 | }); 13 | -------------------------------------------------------------------------------- /test/worker/ungap/decode.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'https://esm.run/@ungap/structured-clone/json'; 2 | import Benchmark from '../benchmark.js'; 3 | 4 | export let decoded = null; 5 | 6 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 7 | if (ACTION === Benchmark.RUN) { 8 | const [data] = rest; 9 | decoded = parse(data); 10 | postMessage([ACTION, data]); 11 | } 12 | else postMessage([Benchmark.INIT]); 13 | }); 14 | -------------------------------------------------------------------------------- /test/worker/ungap/serialization.js: -------------------------------------------------------------------------------- 1 | import { parse, stringify } from 'https://esm.run/@ungap/structured-clone/json'; 2 | 3 | import Benchmark from '../benchmark.js'; 4 | 5 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 6 | if (ACTION === Benchmark.RUN) { 7 | const [data] = rest; 8 | const json = parse(data); 9 | postMessage([ACTION, stringify(json)]); 10 | } 11 | else postMessage([Benchmark.INIT]); 12 | }); 13 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class JSPack { 2 | constructor(options?: { 3 | littleEndian: boolean; 4 | circular: boolean; 5 | byteOffset: number; 6 | byteLength: number; 7 | useFloat32: boolean; 8 | useUTF16: boolean; 9 | mirrored: any[]; 10 | }); 11 | decode: (view: Uint8Array) => any; 12 | encode: (value: any, into?: boolean | ArrayBufferLike) => Uint8Array | number; 13 | } 14 | -------------------------------------------------------------------------------- /test/worker/buffered/serialization.js: -------------------------------------------------------------------------------- 1 | import { encode, decode } from '../../../src/index.js'; 2 | import Benchmark from '../benchmark.js'; 3 | 4 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 5 | if (ACTION === Benchmark.RUN) { 6 | const [data, options] = rest; 7 | const json = encode(decode(data, options), options); 8 | postMessage([ACTION, json], [json.buffer]); 9 | } 10 | else postMessage([Benchmark.INIT]); 11 | }); 12 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Buffered Clone SharedArrayBuffer Benchmark 7 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/worker/jspack/decode.js: -------------------------------------------------------------------------------- 1 | import { decoder } from '../../../../node_modules/jspack/src/decoder.js'; 2 | 3 | import Benchmark from '../benchmark.js'; 4 | 5 | const decode = decoder(); 6 | 7 | export let decoded = null; 8 | 9 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 10 | if (ACTION === Benchmark.RUN) { 11 | const [data] = rest; 12 | decoded = decode(data); 13 | postMessage([ACTION, data], [data.buffer]); 14 | } 15 | else postMessage([Benchmark.INIT]); 16 | }); 17 | -------------------------------------------------------------------------------- /test/sab/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Buffered Clone SharedArrayBuffer Benchmark 7 | 8 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/sab/buffered/roundtrip.js: -------------------------------------------------------------------------------- 1 | import { decode } from '../../../src/index.js'; 2 | 3 | const { BYTES_PER_ELEMENT: I32 } = Int32Array; 4 | const maxByteLength = (2 ** 31) - 1; 5 | 6 | postMessage('ready'); 7 | 8 | addEventListener('message', () => { 9 | let sab = new SharedArrayBuffer(I32, { maxByteLength }); 10 | const i32a = new Int32Array(sab); 11 | postMessage(['encode', sab]); 12 | Atomics.wait(i32a, 0); 13 | const value = i32a[0] < 0 ? [] : i32a[0] < 2 ? [0] : new Uint8Array(sab); 14 | postMessage(['verify', decode(value)]); 15 | }); 16 | -------------------------------------------------------------------------------- /test/worker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Buffered Clone Worker Benchmark 7 | 13 | 14 | 15 | 16 |
⏱️ loading libraries
17 | 18 | 19 | -------------------------------------------------------------------------------- /test/worker/bson/serialization.js: -------------------------------------------------------------------------------- 1 | // import { EJSON } from 'https://esm.run/bson'; 2 | import Benchmark from '../benchmark.js'; 3 | 4 | let BSON; 5 | 6 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 7 | if (ACTION === Benchmark.RUN) { 8 | const [data] = rest; 9 | const json = BSON.deserialize(data); 10 | postMessage([ACTION, BSON.serialize(json)]); 11 | } 12 | else { 13 | import('https://esm.run/bson').then(module => { 14 | ({ BSON } = module); 15 | postMessage([Benchmark.INIT]); 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /test/worker/messagepack/serialization.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | import { Encoder, Decoder } from 'https://esm.run/@webreflection/messagepack@0.0.3'; 4 | 5 | const { encode: wrEncode } = new Encoder({ initialBufferSize: 0xFFFF }); 6 | const { decode: wrDecode } = new Decoder(); 7 | 8 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 9 | if (ACTION === Benchmark.RUN) { 10 | const [data] = rest; 11 | const json = wrDecode(data); 12 | postMessage([ACTION, wrEncode(json)]); 13 | } 14 | else postMessage([Benchmark.INIT]); 15 | }); 16 | -------------------------------------------------------------------------------- /test/worker/jspack/serialization.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | import JSPack from '../../../../node_modules/jspack/src/index.js'; 4 | 5 | const { encode, decode } = new JSPack; 6 | 7 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 8 | if (ACTION === Benchmark.RUN) { 9 | // const [data] = rest; 10 | // const json = decode(data); 11 | // postMessage([ACTION, encode(json)]); 12 | const [data] = rest; 13 | const json = encode(decode(data)); 14 | postMessage([ACTION, json], [json.buffer]); 15 | } 16 | else postMessage([Benchmark.INIT]); 17 | }); 18 | -------------------------------------------------------------------------------- /src/views.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { 4 | DataView, 5 | 6 | Uint8Array, 7 | Uint8ClampedArray, 8 | Uint16Array, 9 | Uint32Array, 10 | Float16Array, 11 | Float32Array, 12 | Float64Array, 13 | Int8Array, 14 | Int16Array, 15 | Int32Array, 16 | BigUint64Array, 17 | BigInt64Array, 18 | } from './globals.js'; 19 | 20 | export default [ 21 | Uint8Array, 22 | Uint8ClampedArray, 23 | Uint16Array, 24 | Uint32Array, 25 | Float16Array, 26 | Float32Array, 27 | Float64Array, 28 | Int8Array, 29 | Int16Array, 30 | Int32Array, 31 | BigUint64Array, 32 | BigInt64Array, 33 | DataView, 34 | ]; 35 | -------------------------------------------------------------------------------- /test/worker/jspack/serialization-nc.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | import JSPack from '../../../../node_modules/jspack/src/index.js'; 4 | 5 | const { encode, decode } = new JSPack({ circular: false }); 6 | 7 | addEventListener('message', ({ data: [ACTION, ...rest] }) => { 8 | if (ACTION === Benchmark.RUN) { 9 | // const [data] = rest; 10 | // const json = decode(data); 11 | // postMessage([ACTION, encode(json)]); 12 | const [data] = rest; 13 | const json = encode(decode(data)); 14 | postMessage([ACTION, json], [json.buffer]); 15 | } 16 | else postMessage([Benchmark.INIT]); 17 | }); 18 | -------------------------------------------------------------------------------- /test/sab/ungap/roundtrip.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'https://esm.run/@ungap/structured-clone/json'; 2 | 3 | const { BYTES_PER_ELEMENT: I32 } = Int32Array; 4 | const maxByteLength = (2 ** 31) - 1; 5 | 6 | const decoder = new TextDecoder; 7 | 8 | postMessage('ready'); 9 | 10 | addEventListener('message', () => { 11 | let sab = new SharedArrayBuffer(I32, { maxByteLength }); 12 | postMessage(['encode', sab]); 13 | const i32a = new Int32Array(sab); 14 | Atomics.wait(i32a, 0); 15 | const length = i32a[0]; 16 | const value = new Uint8Array(sab).slice(I32, I32 + length); 17 | postMessage(['verify', parse(decoder.decode(value))]); 18 | }); 19 | -------------------------------------------------------------------------------- /types/decoder.d.ts: -------------------------------------------------------------------------------- 1 | export function decoder({ littleEndian, circular, mirrored, }?: { 2 | littleEndian: boolean; 3 | circular: boolean; 4 | byteOffset: number; 5 | byteLength: number; 6 | useFloat32: boolean; 7 | useUTF16: boolean; 8 | mirrored: any[]; 9 | }): (view: Uint8Array) => any; 10 | export class Decoder { 11 | constructor(options?: { 12 | littleEndian: boolean; 13 | circular: boolean; 14 | byteOffset: number; 15 | byteLength: number; 16 | useFloat32: boolean; 17 | useUTF16: boolean; 18 | mirrored: any[]; 19 | }); 20 | decode: (view: Uint8Array) => any; 21 | } 22 | -------------------------------------------------------------------------------- /test/worker/structured/double.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | let queue = Promise.withResolvers(); 4 | 5 | const worker = new Worker('./roundtrip.js', { type: 'module' }); 6 | 7 | worker.addEventListener('message', ({ data: [ACTION, ...rest] }) => { 8 | queue.resolve([ACTION, ...rest]); 9 | queue = Promise.withResolvers(); 10 | }); 11 | 12 | addEventListener('message', async ({ data: [ACTION, ...rest] }) => { 13 | if (ACTION === Benchmark.RUN) { 14 | const [data] = rest; 15 | worker.postMessage([ACTION, data]); 16 | } 17 | else worker.postMessage([Benchmark.INIT]); 18 | const result = await queue.promise; 19 | postMessage(result); 20 | }); 21 | -------------------------------------------------------------------------------- /test/worker/structured/triple.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | let queue = Promise.withResolvers(); 4 | 5 | const worker = new Worker('./double.js', { type: 'module' }); 6 | 7 | worker.addEventListener('message', ({ data: [ACTION, ...rest] }) => { 8 | queue.resolve([ACTION, ...rest]); 9 | queue = Promise.withResolvers(); 10 | }); 11 | 12 | addEventListener('message', async ({ data: [ACTION, ...rest] }) => { 13 | if (ACTION === Benchmark.RUN) { 14 | const [data] = rest; 15 | worker.postMessage([ACTION, data]); 16 | } 17 | else worker.postMessage([Benchmark.INIT]); 18 | const result = await queue.promise; 19 | postMessage(result); 20 | }); 21 | -------------------------------------------------------------------------------- /test/sab/buffered/double-roundtrip.js: -------------------------------------------------------------------------------- 1 | import { decode } from '../../../src/index.js'; 2 | 3 | const { BYTES_PER_ELEMENT: I32 } = Int32Array; 4 | 5 | postMessage('ready'); 6 | 7 | let uid = 0; 8 | 9 | addEventListener('message', () => { 10 | const id = uid++; 11 | let sab = new SharedArrayBuffer(I32 * 2); 12 | let i32a = new Int32Array(sab); 13 | postMessage(['length', sab, id]); 14 | Atomics.wait(i32a, 0); 15 | let length = i32a[1]; 16 | sab = new SharedArrayBuffer(length + (I32 - (length % I32))); 17 | i32a = new Int32Array(sab); 18 | postMessage(['encode', sab, id]); 19 | Atomics.wait(i32a, 0); 20 | const value = new Uint8Array(sab); 21 | postMessage(['verify', decode(value)]); 22 | }); 23 | -------------------------------------------------------------------------------- /types/encoder.d.ts: -------------------------------------------------------------------------------- 1 | export function encoder({ littleEndian, circular, byteOffset, byteLength, useFloat32, useUTF16, mirrored, buffer, }?: { 2 | littleEndian: boolean; 3 | circular: boolean; 4 | byteOffset: number; 5 | byteLength: number; 6 | useFloat32: boolean; 7 | useUTF16: boolean; 8 | mirrored: any[]; 9 | }): (value: any, into?: boolean | ArrayBufferLike) => Uint8Array | number; 10 | export class Encoder { 11 | constructor(options?: { 12 | littleEndian: boolean; 13 | circular: boolean; 14 | byteOffset: number; 15 | byteLength: number; 16 | useFloat32: boolean; 17 | useUTF16: boolean; 18 | mirrored: any[]; 19 | }); 20 | encode: (value: any, into?: boolean | ArrayBufferLike) => Uint8Array | number; 21 | } 22 | -------------------------------------------------------------------------------- /test/sab/coincident/roundtrip.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'https://esm.run/@ungap/structured-clone/json'; 2 | 3 | const { BYTES_PER_ELEMENT: I32 } = Int32Array; 4 | 5 | const decoder = new TextDecoder; 6 | 7 | postMessage('ready'); 8 | 9 | let uid = 0; 10 | 11 | addEventListener('message', () => { 12 | const id = uid++; 13 | let sab = new SharedArrayBuffer(I32 * 2); 14 | let i32a = new Int32Array(sab); 15 | postMessage(['length', sab, id]); 16 | Atomics.wait(i32a, 0); 17 | let length = i32a[1]; 18 | sab = new SharedArrayBuffer(length + (I32 - (length % I32))); 19 | i32a = new Int32Array(sab); 20 | postMessage(['encode', sab, id]); 21 | Atomics.wait(i32a, 0); 22 | const value = new Uint8Array(sab).slice(0, length); 23 | postMessage(['verify', parse(decoder.decode(value))]); 24 | }); 25 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | import { encode, decode } from '../../src/index.js'; 2 | 3 | let canvas = document.createElement('canvas'); 4 | canvas.width = 320; 5 | canvas.height = 200; 6 | 7 | const context = canvas.getContext('2d'); 8 | context.fillStyle = '#159'; 9 | context.fillRect(0, 0, canvas.width, canvas.height); 10 | 11 | const imageData = context.getImageData(0, 0, canvas.width, canvas.height); 12 | 13 | const [a, clone, b, _, c] = decode(encode(['a', imageData, 'b', imageData, 'c'])); 14 | console.assert(a === 'a'); 15 | console.assert(b === 'b'); 16 | console.assert(c === 'c'); 17 | console.assert(clone === _); 18 | canvas = document.createElement('canvas'); 19 | canvas.width = clone.width; 20 | canvas.height = clone.height; 21 | canvas.getContext('2d').putImageData(clone, 0, 0); 22 | 23 | document.body.appendChild(canvas); 24 | -------------------------------------------------------------------------------- /test/worker/buffered/triple.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | let queue = Promise.withResolvers(); 4 | 5 | const worker = new Worker('./double.js', { type: 'module' }); 6 | 7 | worker.addEventListener('message', ({ data: [ACTION, ...rest] }) => { 8 | if (ACTION === Benchmark.RUN) { 9 | const [data] = rest; 10 | queue.resolve([[ACTION, data], [data.buffer]]); 11 | } 12 | else queue.resolve([[Benchmark.INIT]]); 13 | queue = Promise.withResolvers(); 14 | }); 15 | 16 | addEventListener('message', async ({ data: [ACTION, ...rest] }) => { 17 | if (ACTION === Benchmark.RUN) { 18 | const [data] = rest; 19 | worker.postMessage([ACTION, data], [data.buffer]); 20 | } 21 | else worker.postMessage([Benchmark.INIT]); 22 | const result = await queue.promise; 23 | postMessage(...result); 24 | }); 25 | -------------------------------------------------------------------------------- /test/worker/buffered/double.js: -------------------------------------------------------------------------------- 1 | import Benchmark from '../benchmark.js'; 2 | 3 | let queue = Promise.withResolvers(); 4 | 5 | const worker = new Worker('./roundtrip.js', { type: 'module' }); 6 | 7 | worker.addEventListener('message', ({ data: [ACTION, ...rest] }) => { 8 | if (ACTION === Benchmark.RUN) { 9 | const [data] = rest; 10 | queue.resolve([[ACTION, data], [data.buffer]]); 11 | } 12 | else queue.resolve([[Benchmark.INIT]]); 13 | queue = Promise.withResolvers(); 14 | }); 15 | 16 | addEventListener('message', async ({ data: [ACTION, ...rest] }) => { 17 | if (ACTION === Benchmark.RUN) { 18 | const [data] = rest; 19 | worker.postMessage([ACTION, data], [data.buffer]); 20 | } 21 | else worker.postMessage([Benchmark.INIT]); 22 | const result = await queue.promise; 23 | postMessage(...result); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [22] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm run build --if-present 26 | - run: npm test 27 | - run: npm run coverage --if-present 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /src/globals.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | class Nope {} 4 | 5 | const MAX_ARGS = 0xFFFF; 6 | 7 | const { 8 | Array, 9 | ArrayBuffer, 10 | DataView, 11 | Date, 12 | Error, 13 | Map, 14 | Number, 15 | Object, 16 | RegExp, 17 | Set, 18 | String, 19 | TypeError, 20 | ImageData = Nope, 21 | 22 | Uint8Array, 23 | Uint8ClampedArray, 24 | Uint16Array, 25 | Uint32Array, 26 | //@ts-ignore 27 | Float16Array = Nope, 28 | Float32Array, 29 | Float64Array, 30 | Int8Array, 31 | Int16Array, 32 | Int32Array, 33 | BigUint64Array, 34 | BigInt64Array, 35 | } = globalThis; 36 | 37 | export { 38 | MAX_ARGS, 39 | Array, 40 | ArrayBuffer, 41 | DataView, 42 | Date, 43 | Error, 44 | Map, 45 | Number, 46 | Object, 47 | RegExp, 48 | Set, 49 | String, 50 | TypeError, 51 | ImageData, 52 | 53 | Uint8Array, 54 | Uint8ClampedArray, 55 | Uint16Array, 56 | Uint32Array, 57 | Float16Array, 58 | Float32Array, 59 | Float64Array, 60 | Int8Array, 61 | Int16Array, 62 | Int32Array, 63 | BigUint64Array, 64 | BigInt64Array, 65 | }; 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2025-today, Andrea Giammarchi, @WebReflection 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the “Software”), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { ArrayBuffer } from '../src/globals.js'; 2 | import BufferedClone from '../src/index.js'; 3 | import { data } from './data.js'; 4 | 5 | import './cover.js'; 6 | 7 | // const buffer = new ArrayBuffer(0xFFFF); 8 | 9 | // const {} 10 | 11 | // let jsp = new JSPack({ 12 | // mirrored: ["test"], 13 | // buffer, 14 | // byteOffset: 4, 15 | // useUTF16: true, 16 | // circular: false, 17 | // // minimumByteLength: 1, 18 | // }); 19 | 20 | // console.log(jsp.encode(null, true)); 21 | 22 | // console.log(jsp.decode(jsp.encode("!".repeat(10)))); 23 | 24 | // console.log(jsp.decode(jsp.encode(["💩", "💩"]))); 25 | 26 | // console.log(jsp.decode(jsp.encode(new Uint16Array([1, 2])))); 27 | 28 | // console.log(jsp.decode(jsp.encode("a")), jsp.decode(jsp.encode("🥳"))); 29 | 30 | // console.log(jsp.decode(jsp.encode([{"test": "value"}, "test"]))); 31 | // console.log(jsp.decode(jsp.encode(new Map([['a', 1]])))); 32 | // console.log(jsp.decode(jsp.encode(new Set(['a', 1])))); 33 | // console.log(jsp.decode(jsp.encode(/re/g))); 34 | // console.log(jsp.decode(jsp.encode(new TypeError("test")))); 35 | 36 | // console.log(jsp.decode(jsp.encode(data))); 37 | -------------------------------------------------------------------------------- /test/worker/benchmark.js: -------------------------------------------------------------------------------- 1 | export default class Benchmark { 2 | static INIT = 0; 3 | static RUN = 1; 4 | 5 | #ready = false; 6 | #send; 7 | #name; 8 | #queue; 9 | #worker; 10 | 11 | constructor(options) { 12 | this.#send = (...args) => options.send(...args); 13 | this.#worker = new Worker(options.url, { type: 'module' }); 14 | this.#worker.addEventListener('message', this); 15 | this.#queue = Promise.withResolvers(); 16 | this.#worker.postMessage([Benchmark.INIT]); 17 | } 18 | 19 | get ready() { return this.#queue.promise } 20 | 21 | handleEvent({ data: [ACTION, ...rest] }) { 22 | if (ACTION === Benchmark.RUN) 23 | console.timeEnd(this.#name); 24 | else this.#ready = true; 25 | this.#queue.resolve(...rest); 26 | } 27 | 28 | run(name) { 29 | if (!this.#ready) throw new Error('benchmark not ready'); 30 | this.#name = name; 31 | this.#queue = Promise.withResolvers(); 32 | console.time(this.#name); 33 | const [args, ...rest] = this.#send(); 34 | this.#worker.postMessage([Benchmark.RUN, ...args], ...rest); 35 | return this.#queue.promise; 36 | } 37 | 38 | terminate() { 39 | this.#worker.terminate(); 40 | this.#ready = false; 41 | this.#queue.reject('terminated before resolution'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mini-coi.js: -------------------------------------------------------------------------------- 1 | /*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ 2 | /*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */ 3 | (({ document: d, navigator: { serviceWorker: s } }) => { 4 | if (d) { 5 | const { currentScript: c } = d; 6 | s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => { 7 | r.addEventListener('updatefound', () => location.reload()); 8 | if (r.active && !s.controller) location.reload(); 9 | }); 10 | } 11 | else { 12 | addEventListener('install', () => skipWaiting()); 13 | addEventListener('activate', e => e.waitUntil(clients.claim())); 14 | addEventListener('fetch', e => { 15 | const { request: r } = e; 16 | if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return; 17 | e.respondWith(fetch(r).then(r => { 18 | const { body, status, statusText } = r; 19 | if (!status || status > 399) return r; 20 | const h = new Headers(r.headers); 21 | h.set('Cross-Origin-Opener-Policy', 'same-origin'); 22 | h.set('Cross-Origin-Embedder-Policy', 'require-corp'); 23 | h.set('Cross-Origin-Resource-Policy', 'cross-origin'); 24 | return new Response(body, { status, statusText, headers: h }); 25 | })); 26 | }); 27 | } 28 | })(self); 29 | -------------------------------------------------------------------------------- /test/index_old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | import { data, verify } from './data.js'; 2 | // import { encode, decode } from '../src/index.js'; 3 | 4 | import JSPack from '../node_modules/jspack/src/index.js'; 5 | 6 | const { encode, decode } = new JSPack; 7 | 8 | // const data = [1, 2, 3]; 9 | // data.push(data); 10 | // const verify = clone => clone.length === data.length && clone[3] === clone; 11 | 12 | console.time('cold structuredClone'); 13 | var clone = structuredClone(data); 14 | console.timeEnd('cold structuredClone'); 15 | verify(clone); 16 | 17 | for (let i = 0; i < 10; i++) 18 | verify(structuredClone(data)); 19 | 20 | console.time('hot structuredClone'); 21 | var clone = structuredClone(data); 22 | console.timeEnd('hot structuredClone'); 23 | verify(clone); 24 | 25 | console.time('cold bufferedClone'); 26 | var clone = decode(encode(data)); 27 | console.timeEnd('cold bufferedClone'); 28 | verify(clone); 29 | 30 | for (let i = 0; i < 10; i++) 31 | verify(decode(encode(data))); 32 | 33 | console.time('hot bufferedClone'); 34 | var clone = decode(encode(data)); 35 | console.timeEnd('hot bufferedClone'); 36 | verify(clone); 37 | 38 | console.time('encode bufferedClone'); 39 | var encoded = encode(data); 40 | console.timeEnd('encode bufferedClone'); 41 | verify(decode(encoded)); 42 | 43 | console.time('decode bufferedClone'); 44 | var clone = decode(encoded); 45 | console.timeEnd('decode bufferedClone'); 46 | verify(clone); 47 | -------------------------------------------------------------------------------- /types/globals.d.ts: -------------------------------------------------------------------------------- 1 | export const MAX_ARGS: 65535; 2 | export const Array: ArrayConstructor; 3 | export const ArrayBuffer: ArrayBufferConstructor; 4 | export const DataView: DataViewConstructor; 5 | export const Date: DateConstructor; 6 | export const Error: ErrorConstructor; 7 | export const Map: MapConstructor; 8 | export const Number: NumberConstructor; 9 | export const Object: ObjectConstructor; 10 | export const RegExp: RegExpConstructor; 11 | export const Set: SetConstructor; 12 | export const String: StringConstructor; 13 | export const TypeError: TypeErrorConstructor; 14 | export const ImageData: { 15 | new (sw: number, sh: number, settings?: ImageDataSettings): ImageData; 16 | new (data: Uint8ClampedArray, sw: number, sh?: number, settings?: ImageDataSettings): ImageData; 17 | prototype: ImageData; 18 | } | typeof Nope; 19 | export const Uint8Array: Uint8ArrayConstructor; 20 | export const Uint8ClampedArray: Uint8ClampedArrayConstructor; 21 | export const Uint16Array: Uint16ArrayConstructor; 22 | export const Uint32Array: Uint32ArrayConstructor; 23 | export const Float16Array: any; 24 | export const Float32Array: Float32ArrayConstructor; 25 | export const Float64Array: Float64ArrayConstructor; 26 | export const Int8Array: Int8ArrayConstructor; 27 | export const Int16Array: Int16ArrayConstructor; 28 | export const Int32Array: Int32ArrayConstructor; 29 | export const BigUint64Array: BigUint64ArrayConstructor; 30 | export const BigInt64Array: BigInt64ArrayConstructor; 31 | declare class Nope { 32 | } 33 | export {}; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buffered-clone", 3 | "version": "0.8.3", 4 | "type": "module", 5 | "types": "./types/index.d.ts", 6 | "main": "./src/index.js", 7 | "module": "./src/index.js", 8 | "scripts": { 9 | "build": "npm run ts && npm run test", 10 | "test": "c8 node ./test/cover.js", 11 | "ts": "tsc --allowJs --checkJs --lib dom,esnext --target esnext -d --emitDeclarationOnly --outDir ./types ./src/index.js", 12 | "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info" 13 | }, 14 | "exports": { 15 | ".": { 16 | "import": "./src/index.js", 17 | "types": "./types/index.d.ts" 18 | }, 19 | "./decoder": { 20 | "import": "./src/decoder.js", 21 | "types": "./types/decoder.d.ts" 22 | }, 23 | "./encoder": { 24 | "import": "./src/encoder.js", 25 | "types": "./types/encoder.d.ts" 26 | }, 27 | "./package.json": "./package.json" 28 | }, 29 | "files": [ 30 | "src/*", 31 | "types/*", 32 | "README.md", 33 | "LICENSE" 34 | ], 35 | "keywords": [ 36 | "structured", 37 | "clone", 38 | "buffer", 39 | "binary" 40 | ], 41 | "author": "Andrea Giammarchi", 42 | "license": "MIT", 43 | "description": "A structured clone equivalent able to encode and decode as a buffer", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/WebReflection/buffered-clone.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/WebReflection/buffered-clone/issues" 50 | }, 51 | "homepage": "https://github.com/WebReflection/buffered-clone#readme", 52 | "devDependencies": { 53 | "@msgpack/msgpack": "^3.0.1", 54 | "@webreflection/messagepack": "^0.0.2", 55 | "c8": "^10.1.3", 56 | "typescript": "^5.7.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/worker/index.js: -------------------------------------------------------------------------------- 1 | import { bold, light } from 'https://esm.run/console-strings'; 2 | import * as console from 'https://esm.run/console-strings/browser'; 3 | 4 | import Benchmark from './benchmark.js'; 5 | import tests from './tests.js'; 6 | 7 | const lighter = globalThis.chrome ? light : String; 8 | 9 | const append = (type, textContent) => benchmark.appendChild( 10 | el(type, textContent) 11 | ); 12 | 13 | const el = (type, textContent) => Object.assign( 14 | document.createElement(type), { textContent } 15 | ); 16 | 17 | const sleep = ms => new Promise($ => setTimeout($, ms)); 18 | 19 | benchmark.replaceChildren(); 20 | for (const [test, runs] of Object.entries(tests)) { 21 | append('h3', test); 22 | console.log(bold(test.toUpperCase())); 23 | let ok = 1; 24 | for (const run of runs) { 25 | // if (run.name.includes('@webreflection')) debugger; 26 | const p = append('p'); 27 | const small = el('small', ` @ ${run.url} `); 28 | p.append(el('strong', run.name), small, el('br')); 29 | console.log(` ${bold(run.name)}`); 30 | let checks = 0; 31 | const bench = new Benchmark(run); 32 | const info = p.appendChild(el('span', '⏱️ testing')); 33 | const check = data => { 34 | checks++; 35 | ok = run.verify(run.decode?.(data) ?? data) ?? 1; 36 | }; 37 | await bench.ready; 38 | globalThis.console.time(` • total`); 39 | let now = performance.now(); 40 | bench.run(` • ${lighter('cold run')} `); 41 | await bench.ready.then(check); 42 | for (let i = 0; i < run.hot; i++) { 43 | bench.run(` • ${lighter(`hot run ${i + 1}`)}`); 44 | await bench.ready.then(check); 45 | } 46 | bench.terminate(); 47 | now = performance.now() - now; 48 | globalThis.console.timeEnd(` • total`); 49 | const emoji = ok > 0 ? '✅' : (!ok ? '⚠️' : '🚫'); 50 | const prefix = checks === (run.hot + 1) ? `${emoji} done` : `🚫 failed`; 51 | const suffix = `(1 cold + ${run.hot} hot runs)`; 52 | info.textContent = `${prefix} with ${checks} checks ${suffix} in ${now.toFixed(2)}ms`; 53 | } 54 | append('hr'); 55 | console.log(''); 56 | await sleep(500); 57 | } 58 | append('h3', '✅ Done'); 59 | -------------------------------------------------------------------------------- /old/encode/utils.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | /** @typedef {import("../encode.js").RAM} RAM */ 4 | /** @typedef {import("../encode.js").Recursion} Recursion */ 5 | 6 | /** 7 | * @param {RAM} RAM 8 | * @param {number} type 9 | * @param {string} str 10 | */ 11 | export const asASCII = (RAM, type, str) => { 12 | const { length } = str; 13 | pushLength(RAM, type, length); 14 | let { _, a, $ } = RAM; 15 | if ($) { 16 | //@ts-ignore 17 | a.buffer.resize(_ + length); 18 | // ⚠️ this cannot be done with a resizable buffer: WHY?!? 19 | // ⚠️ this likely cannot be done with a SharedArrayBuffer too! 20 | // encoder.encodeInto(str, a.subarray(_)); 21 | // RAM._ += length; 22 | } 23 | for (let i = 0; i < length; i++) 24 | a[_++] = str.charCodeAt(i); 25 | RAM._ = _; 26 | }; 27 | 28 | /** 29 | * @param {any} value 30 | * @returns 31 | */ 32 | export const asValid = value => { 33 | const type = typeof value; 34 | switch (type) { 35 | case 'symbol': 36 | case 'function': 37 | case 'undefined': return ''; 38 | default: return type; 39 | } 40 | }; 41 | 42 | import { unsigned } from '../number.js'; 43 | 44 | /** 45 | * @param {RAM|Recursion} RAM 46 | * @param {number} type 47 | * @param {number} length 48 | */ 49 | export const pushLength = (RAM, type, length) => { 50 | const [t, v] = unsigned(length); 51 | let { _, a, $ } = RAM, len = v.length; 52 | //@ts-ignore 53 | if ($) a.buffer.resize(_ + len + 2); 54 | a[_++] = type; 55 | a[_++] = t; 56 | for (let i = 0; i < len; i++) a[_++] = v[i]; 57 | RAM._ = _; 58 | }; 59 | 60 | /** 61 | * @param {RAM} RAM 62 | * @param {number} value 63 | */ 64 | export const pushValue = (RAM, value) => { 65 | let { _, a, $ } = RAM; 66 | //@ts-ignore 67 | if ($) a.buffer.resize(_ + 1); 68 | a[RAM._++] = value; 69 | }; 70 | 71 | /** 72 | * @param {RAM} RAM 73 | * @param {number[]|Uint8Array} values 74 | */ 75 | export const pushValues = (RAM, values) => { 76 | let { _, a, $ } = RAM, i = 0, length = values.length; 77 | //@ts-ignore 78 | if ($) a.buffer.resize(_ + length); 79 | while (i < length) a[_++] = values[i++]; 80 | RAM._ = _; 81 | }; 82 | 83 | /** 84 | * @param {RAM} RAM 85 | * @param {number[]|Uint8Array} view 86 | */ 87 | export const pushView = (RAM, view) => { 88 | let { _, a, $ } = RAM, length = view.length; 89 | //@ts-ignore 90 | if ($) a.buffer.resize(_ + length); 91 | /** @type {Uint8Array} */(a).set(view, _); 92 | RAM._ += length; 93 | }; 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # buffered-clone 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/WebReflection/buffered-clone/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/buffered-clone?branch=main) 4 | 5 | **Social Media Photo by [marc belver colomer](https://unsplash.com/@marc_belver) on [Unsplash](https://unsplash.com/)** 6 | 7 | A [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) like utility that converts all [supported JS types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#javascript_types), plus [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData), into a binary format. 8 | 9 | **Highlights** 10 | 11 | * recursive out of the box for almost anything that can be serialized 12 | * once *hot*, it's nearly as fast as native *structuredClone* 13 | * it allows filling pre-allocated buffers and *SharedArrayBuffer* 14 | * it allows growing *buffers* if resizable 15 | * supports `toJSON` (already) and *MessagePack* extensions like mechanism (coming soon) 16 | 17 | - - - 18 | 19 | ## API 20 | 21 | ```js 22 | import BufferedClone from 'buffered-clone'; 23 | 24 | const { encode, decode } = new BufferedClone({ 25 | // it's feature detected at runtime, don't change it 26 | // unless you know what you are doing. 27 | littleEndian: true, 28 | // by default makes references and strings encoded once 29 | circular: true, 30 | // if a view has already reserved buffer size, 31 | // this can be used to offset the encoding 32 | byteOffset: 0, 33 | // either the initial buffer length, when not provided, 34 | // or the amount of RAM to ask per each resize on top 35 | // of the new required size (incremental grow): the smaller 36 | // this value is, the least RAM is used but the slowest 37 | // serialization happens while encoding (due multiple resizes) 38 | byteLength: 0x1000000, 39 | // forces usage of Float 32 numbers instead of 40 | // the JS default which is Float 64 41 | useFloat32: false, 42 | // encodes strings directly as UTF16 without 43 | // needing any UTF16 to UTF8 conversion. 44 | // it is usually faster than UTF8 encode + view.set(...) 45 | // and it can deal with SharedArrayBuffer or resizable 46 | // ArrayBuffer without throwing, ideal for encodeInto case 47 | useUTF16: false, 48 | // mirrors common known strings or values 49 | // across worlds: it must be a precise list 50 | mirrored: [], 51 | // it can be a growable SharedArrayBuffer 52 | // or a resizable ArrayBuffer 53 | // or just nothing, defaulting to: 54 | buffer: new ArrayBuffer(0x1000000) 55 | }); 56 | 57 | // returns a Uint8Array of the serialized data 58 | encode(anyCompatibleValue); 59 | 60 | 61 | // encodes *into* the currently available buffer 62 | encode(anyCompatibleValue, true); 63 | // encodes *into* a different buffer (discards the previous) 64 | encode(anyCompatibleValue, specificBuffer); 65 | 66 | // returns any compatible value that was serialized 67 | decode(ui8a); 68 | ``` 69 | -------------------------------------------------------------------------------- /old/constants.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | /** 4 | * @param {string} c 5 | * @returns 6 | */ 7 | const $ = c => c.charCodeAt(0); 8 | 9 | export const NULL = 0; 10 | 11 | export const BUFFER = /** @type {66} */( $('B') ); 12 | export const BIGINT = /** @type {73} */( $('I') ); 13 | export const NUMBER = /** @type {110} */( $('n') ); 14 | export const ARRAY = /** @type {65} */( $('A') ); 15 | export const RECURSIVE = /** @type {114} */( $('r') ); 16 | export const SYMBOL = /** @type {121} */( $('y') ); 17 | 18 | export const DATE = /** @type {68} */( $('D') ); 19 | export const OBJECT = /** @type {79} */( $('O') ); 20 | export const UNDEFINED = /** @type {48} */( $('0') ); 21 | export const MAP = /** @type {77} */( $('M') ); 22 | export const SET = /** @type {83} */( $('S') ); 23 | export const TYPED = /** @type {84} */( $('T') ); 24 | export const ERROR = /** @type {101} */( $('e') ); 25 | export const REGEXP = /** @type {82} */( $('R') ); 26 | export const STRING = /** @type {115} */( $('s') ); 27 | 28 | export const ASCII = /** @type {97} */( $('a') ); 29 | export const FALSE = /** @type {98} */( $('b') ); 30 | export const TRUE = /** @type {99} */( $('c') ); 31 | export const IMAGEDATA = /** @type {100} */( $('d') ); 32 | export const FUNCTION = /** @type {102} */( $('f') ); 33 | export const DATAVIEW = /** @type {118} */( $('v') ); 34 | 35 | // numbers are all over 127 ASCII values 36 | 37 | export const I8A = 128; 38 | export const I8 = 129; 39 | 40 | // space for I8CA 41 | 42 | export const U8A = 132; 43 | export const U8 = 133; 44 | 45 | export const U8CA = 134; 46 | export const U8C = 135; 47 | 48 | export const I16A = 136; 49 | export const I16 = 137; 50 | 51 | // space for Uint16ClampedArray 52 | 53 | export const U16A = 140; 54 | export const U16 = 141; 55 | 56 | export const F16A = 142; 57 | export const F16 = 143; 58 | 59 | export const I32A = 144; 60 | export const I32 = 145; 61 | 62 | // space for Uint32ClampedArray 63 | 64 | export const U32A = 148; 65 | export const U32 = 149; 66 | 67 | // space for consistency sake 68 | 69 | export const F32A = 152; 70 | export const F32 = 153; 71 | 72 | export const F64A = 156; 73 | export const F64 = 157; 74 | 75 | export const I64A = 160; 76 | export const I64 = 161; 77 | 78 | // space for consistency sake 79 | 80 | export const U64A = 164; 81 | export const U64 = 165; 82 | 83 | // space for consistency sake 84 | 85 | 86 | // NOT SUPPORTED IN JS 87 | // export const I128A = 168; 88 | // export const I128 = 169; 89 | 90 | // export const U128A = 172; 91 | // export const U128 = 173; 92 | 93 | // export const F128A = 176; 94 | // export const F128 = 177; 95 | 96 | // export const I256A = 180; 97 | // export const I256 = 181; 98 | 99 | // export const U256A = 184; 100 | // export const U256 = 185; 101 | 102 | // export const F256A = 188; 103 | // export const F256 = 189; 104 | 105 | // export const I512A = 192; 106 | // export const I512 = 193; 107 | 108 | // export const U512A = 194; 109 | // export const U512 = 195; 110 | 111 | // export const F512A = 198; 112 | // export const F512 = 199; 113 | 114 | // ... others ... 115 | 116 | export const MAX_U8 = 2 ** 8; 117 | export const MAX_I8 = MAX_U8 / 2; 118 | 119 | export const MAX_U16 = 2 ** 16; 120 | export const MAX_I16 = MAX_U16 / 2; 121 | 122 | export const MAX_U32 = 2 ** 32; 123 | export const MAX_I32 = MAX_U32 / 2; 124 | 125 | // ⚠️ this is problematic in JS 126 | export const MAX_F32 = 3.4e38; 127 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | const date = new Date; 2 | const buffer = new ArrayBuffer(128); 3 | const error = new SyntaxError('reason'); 4 | const map = new Map; 5 | const regexp = /[a-z0-9:.-]+/gmi; 6 | const set = new Set; 7 | const typed = new Int32Array(buffer); 8 | const dataview = new DataView(buffer); 9 | 10 | const array = []; 11 | const object = {}; 12 | 13 | const data = { 14 | array, 15 | boolean: true, 16 | null: null, 17 | number: 123.456789, 18 | object, 19 | string: String(date), 20 | 21 | // bigint: 123456789n, 22 | buffer, 23 | date, 24 | error, 25 | map, 26 | regexp, 27 | set, 28 | typed, 29 | dataview, 30 | }; 31 | 32 | array.push(data); 33 | object.recursive = data; 34 | 35 | for (let ui8a = new Uint8Array(buffer), i = 0; i < ui8a.length; i++) { 36 | ui8a[i] = i; 37 | const random = Math.random() * i; 38 | set.add(random); 39 | map.set(i, random); 40 | } 41 | 42 | function same(value, i) { 43 | return value === this[i]; 44 | } 45 | 46 | const is = (source, replica, clone, known) => { 47 | if (known.has(replica)) return true; 48 | switch (source) { 49 | case array: { 50 | known.add(replica); 51 | let { length } = source; 52 | if (length-- !== replica.length) 53 | return false; 54 | for (let i = 0; i < length; i++) { 55 | if (!is(source[i], replica[i], clone, known)) 56 | return false; 57 | } 58 | return replica[length] === clone; 59 | } 60 | case object: { 61 | known.add(replica); 62 | const se = [...Object.entries(source)]; 63 | const re = [...Object.entries(replica)]; 64 | if (se.length !== re.length) 65 | return false; 66 | for (let i = 0; i < se.length; i++) { 67 | const [sk, sv] = se[i]; 68 | const [rk, rv] = re[i]; 69 | if (sk !== rk) 70 | return false; 71 | if (sk === 'recursive') { 72 | if (rv !== clone) 73 | return false; 74 | } 75 | else if (!is(sv, rv, clone, known)) 76 | return false; 77 | } 78 | return true; 79 | } 80 | case buffer: { 81 | known.add(replica); 82 | const view = new Int32Array(replica); 83 | if (typed.length !== view.length) 84 | return false; 85 | return typed.every(same, view); 86 | } 87 | case date: { 88 | known.add(replica); 89 | return +source === +replica; 90 | } 91 | case error: { 92 | known.add(replica); 93 | return source.name === replica.name && source.message === replica.message; 94 | } 95 | case map: { 96 | known.add(replica); 97 | if (source.size !== replica.size) 98 | return false; 99 | for (const [k, v] of source) { 100 | if (replica.get(k) !== v) 101 | return false; 102 | } 103 | return true; 104 | } 105 | case regexp: { 106 | known.add(replica); 107 | return source.source === replica.source && source.flags === replica.flags; 108 | } 109 | case set: { 110 | known.add(replica); 111 | if (source.size !== replica.size) 112 | return false; 113 | return [...source].every(same, [...replica]); 114 | } 115 | case typed: { 116 | known.add(replica); 117 | if (source.constructor !== replica.constructor) 118 | return false; 119 | if (source.length !== replica.length) 120 | return false; 121 | return source.every(same, replica); 122 | } 123 | case dataview: { 124 | known.add(replica); 125 | if (source.constructor !== replica.constructor) 126 | return false; 127 | const i32a = new Int32Array(replica.buffer); 128 | if (typed.length !== i32a.length) 129 | return false; 130 | return typed.every(same, i32a); 131 | } 132 | default: return source === replica; 133 | } 134 | }; 135 | 136 | const verify = clone => { 137 | const known = new Set; 138 | for (const key of [ 139 | 'array', 140 | 'object' 141 | ]) { 142 | if (!is(data[key], clone[key], clone, known)) 143 | throw new TypeError(`Invalid ${key}`); 144 | } 145 | for (const key of [ 146 | 'bigint', 147 | 'boolean', 148 | 'null', 149 | 'number', 150 | 'string', 151 | ]) { 152 | if (!is(data[key], clone[key], clone, known)) 153 | throw new TypeError(`Invalid primitive ${key}`); 154 | } 155 | }; 156 | 157 | export { data, verify }; 158 | -------------------------------------------------------------------------------- /test/data_old.js: -------------------------------------------------------------------------------- 1 | const date = new Date; 2 | const buffer = new ArrayBuffer(128); 3 | const error = new SyntaxError('reason'); 4 | const map = new Map; 5 | const regexp = /[a-z0-9:.-]+/gmi; 6 | const set = new Set; 7 | const typed = new Int32Array(buffer); 8 | const dataview = new DataView(buffer); 9 | 10 | const array = []; 11 | const object = {}; 12 | 13 | const data = { 14 | array, 15 | boolean: true, 16 | null: null, 17 | number: 123.456789, 18 | object, 19 | string: String(date), 20 | 21 | // bigint: 123456789n, 22 | buffer, 23 | date, 24 | error, 25 | map, 26 | regexp, 27 | set, 28 | typed, 29 | dataview, 30 | }; 31 | 32 | array.push(data); 33 | object.recursive = data; 34 | 35 | for (let ui8a = new Uint8Array(buffer), i = 0; i < ui8a.length; i++) { 36 | ui8a[i] = i; 37 | const random = Math.random() * i; 38 | set.add(random); 39 | map.set(i, random); 40 | } 41 | 42 | function same(value, i) { 43 | return value === this[i]; 44 | } 45 | 46 | const is = (source, replica, clone, known) => { 47 | if (known.has(replica)) return true; 48 | switch (source) { 49 | case array: { 50 | known.add(replica); 51 | let { length } = source; 52 | if (length-- !== replica.length) 53 | return false; 54 | for (let i = 0; i < length; i++) { 55 | if (!is(source[i], replica[i], clone, known)) 56 | return false; 57 | } 58 | return replica[length] === clone; 59 | } 60 | case object: { 61 | known.add(replica); 62 | const se = [...Object.entries(source)]; 63 | const re = [...Object.entries(replica)]; 64 | if (se.length !== re.length) 65 | return false; 66 | for (let i = 0; i < se.length; i++) { 67 | const [sk, sv] = se[i]; 68 | const [rk, rv] = re[i]; 69 | if (sk !== rk) 70 | return false; 71 | if (sk === 'recursive') { 72 | if (rv !== clone) 73 | return false; 74 | } 75 | else if (!is(sv, rv, clone, known)) 76 | return false; 77 | } 78 | return true; 79 | } 80 | case buffer: { 81 | known.add(replica); 82 | const view = new Int32Array(replica); 83 | if (typed.length !== view.length) 84 | return false; 85 | return typed.every(same, view); 86 | } 87 | case date: { 88 | known.add(replica); 89 | return +source === +replica; 90 | } 91 | case error: { 92 | known.add(replica); 93 | return source.name === replica.name && source.message === replica.message; 94 | } 95 | case map: { 96 | known.add(replica); 97 | if (source.size !== replica.size) 98 | return false; 99 | for (const [k, v] of source) { 100 | if (replica.get(k) !== v) 101 | return false; 102 | } 103 | return true; 104 | } 105 | case regexp: { 106 | known.add(replica); 107 | return source.source === replica.source && source.flags === replica.flags; 108 | } 109 | case set: { 110 | known.add(replica); 111 | if (source.size !== replica.size) 112 | return false; 113 | return [...source].every(same, [...replica]); 114 | } 115 | case typed: { 116 | known.add(replica); 117 | if (source.constructor !== replica.constructor) 118 | return false; 119 | if (source.length !== replica.length) 120 | return false; 121 | return source.every(same, replica); 122 | } 123 | case dataview: { 124 | known.add(replica); 125 | if (source.constructor !== replica.constructor) 126 | return false; 127 | const i32a = new Int32Array(replica.buffer); 128 | if (typed.length !== i32a.length) 129 | return false; 130 | return typed.every(same, i32a); 131 | } 132 | default: return source === replica; 133 | } 134 | }; 135 | 136 | const verify = clone => { 137 | const known = new Set; 138 | for (const key of [ 139 | 'array', 140 | 'object' 141 | ]) { 142 | if (!is(data[key], clone[key], clone, known)) 143 | throw new TypeError(`Invalid ${key}`); 144 | } 145 | for (const key of [ 146 | 'bigint', 147 | 'boolean', 148 | 'null', 149 | 'number', 150 | 'string', 151 | ]) { 152 | if (!is(data[key], clone[key], clone, known)) 153 | throw new TypeError(`Invalid primitive ${key}`); 154 | } 155 | }; 156 | 157 | export { data, verify }; 158 | -------------------------------------------------------------------------------- /old/number.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { 4 | BIGINT, 5 | NUMBER, 6 | 7 | I8, 8 | U8, 9 | I16, 10 | U16, 11 | F16, 12 | I32, 13 | F32, 14 | U32, 15 | I64, 16 | F64, 17 | U64, 18 | 19 | MAX_U8, 20 | MAX_I8, 21 | MAX_U16, 22 | MAX_I16, 23 | MAX_U32, 24 | MAX_I32, 25 | } from './constants.js'; 26 | 27 | import Float16Array from './float16array.js'; 28 | 29 | /** @typedef {I8|U8|I16|U16|F16|I32|F32|U32|I64|F64|U64} Type */ 30 | /** @typedef {Int8Array|Uint8Array|Int16Array|Uint16Array|Float16Array|Int32Array|Float32Array|Uint32Array|BigInt64Array|Float64Array|BigUint64Array} TypedArray */ 31 | 32 | // type "number" 33 | export class Number { 34 | /** 35 | * @param {TypedArray} view 36 | * @param {Uint8Array} ui8a 37 | */ 38 | constructor(view, ui8a) { 39 | this.$ = view; 40 | this._ = ui8a; 41 | this.length = view.BYTES_PER_ELEMENT; 42 | } 43 | /** 44 | * @param {number|bigint} value 45 | * @returns 46 | */ 47 | encode(value) { 48 | this.$[0] = value; 49 | return this._; 50 | } 51 | /** 52 | * @param {Uint8Array} value 53 | * @returns {number|bigint} 54 | */ 55 | decode(value) { 56 | this._.set(value); 57 | return this.$[0]; 58 | } 59 | } 60 | 61 | // buffers 62 | const b1 = new ArrayBuffer(1); 63 | const b2 = new ArrayBuffer(2); 64 | const b4 = new ArrayBuffer(4); 65 | const b8 = new ArrayBuffer(8); 66 | 67 | // unsigned 68 | const ui8b1 = new Uint8Array(b1); 69 | const ui8b2 = new Uint8Array(b2); 70 | const ui8b4 = new Uint8Array(b4); 71 | const ui8b8 = new Uint8Array(b8); 72 | 73 | // signed 74 | export const i8 = new Number(new Int8Array(b1), ui8b1); 75 | export const i16 = new Number(new Int16Array(b2), ui8b2); 76 | export const i32 = new Number(new Int32Array(b4), ui8b4); 77 | 78 | // unsigned 79 | export const u8 = new Number(new Uint8Array(b1), ui8b1); 80 | export const u16 = new Number(new Uint16Array(b2), ui8b2); 81 | export const u32 = new Number(new Uint32Array(b4), ui8b4); 82 | 83 | // float or double precision 84 | export const f16 = new Number(new Float16Array(b2), ui8b2); 85 | export const f32 = new Number(new Float32Array(b4), ui8b4); 86 | export const f64 = new Number(new Float64Array(b8), ui8b8); 87 | 88 | // type "bigint" 89 | export class BigNumber extends Number { 90 | /** 91 | * @param {Uint8Array} value 92 | * @returns {bigint} 93 | */ 94 | decode(value) { 95 | this._.set(value); 96 | return /** @type {bigint} */(this.$[0]); 97 | } 98 | } 99 | 100 | // signed 101 | export const i64 = new BigNumber(new BigInt64Array(b8), ui8b8); 102 | 103 | // unsigned 104 | export const u64 = new BigNumber(new BigUint64Array(b8), ui8b8); 105 | 106 | 107 | 108 | // encode 109 | const { isInteger } = globalThis.Number; 110 | 111 | /** 112 | * @param {number} value 113 | * @returns {[number,Uint8Array]} 114 | */ 115 | const signed = value => ( 116 | -MAX_I8 < value ? [I8, i8.encode(value)] : 117 | -MAX_I16 < value ? [I16, i16.encode(value)] : 118 | -MAX_I32 < value ? [I32, i32.encode(value)] : 119 | [F64, f64.encode(value)] 120 | ); 121 | 122 | /** 123 | * @param {number} value 124 | * @returns {[number,Uint8Array]} 125 | */ 126 | export const unsigned = value => ( 127 | value < MAX_U8 ? [U8, u8.encode(value)] : 128 | value < MAX_U16 ? [U16, u16.encode(value)] : 129 | value < MAX_U32 ? [U32, u32.encode(value)] : 130 | [F64, f64.encode(value)] 131 | ); 132 | 133 | /** 134 | * @param {NUMBER|BIGINT} type 135 | * @param {number|bigint} value 136 | * @returns {[number,Uint8Array]} 137 | */ 138 | export const encode = (type, value) => type === NUMBER ? 139 | //@ts-ignore 140 | (isInteger(value) ? (value < 0 ? signed(value) : unsigned(value)) : [F64, f64.encode(value)]) : 141 | (value < 0n ? [I64, i64.encode(value)] : [U64, u64.encode(value)]) 142 | ; 143 | 144 | // ⚠️ F32 really messes things up losing details 145 | // if (-MAX_F32 < valueOf && valueOf < MAX_F32) { 146 | // return [F32, f32.encode(valueOf)] 147 | // } 148 | 149 | // export const serialize = { 150 | // i8: /** @param {number} value */ value => ([I8, ...i8.encode(value)]), 151 | // i16: /** @param {number} value */ value => ([I16, ...i16.encode(value)]), 152 | // i32: /** @param {number} value */ value => ([I32, ...i32.encode(value)]), 153 | // i64: /** @param {bigint} value */ value => ([I64, ...i64.encode(value)]), 154 | // u8: /** @param {number} value */ value => ([U8, ...u8.encode(value)]), 155 | // u16: /** @param {number} value */ value => ([U16, ...u16.encode(value)]), 156 | // u32: /** @param {number} value */ value => ([U32, ...u32.encode(value)]), 157 | // u64: /** @param {bigint} value */ value => ([U64, ...u64.encode(value)]), 158 | // f32: /** @param {number} value */ value => ([F32, ...f32.encode(value)]), 159 | // f64: /** @param {number} value */ value => ([F64, ...f64.encode(value)]), 160 | // }; 161 | -------------------------------------------------------------------------------- /test/sab/index.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'https://esm.run/@ungap/structured-clone/json'; 2 | import { encode } from '../../src/index.js'; 3 | 4 | import { data, verify } from '../data.js'; 5 | // const data = await (await fetch('../worker/carts.json')).json(); 6 | // const verify = () => true; 7 | 8 | const RUNS = 10; 9 | const Workers = new WeakMap; 10 | 11 | const create = handler => { 12 | const queue = Promise.withResolvers(); 13 | const worker = new Worker(handler.url, { type: 'module' }); 14 | const entry = [worker, queue]; 15 | worker.addEventListener('message', queue.resolve, { once: true }); 16 | Workers.set(handler, entry); 17 | return entry; 18 | }; 19 | 20 | const sleep = ms => new Promise($ => setTimeout($, ms)); 21 | 22 | const terminate = async handler => { 23 | const [worker] = Workers.get(handler); 24 | Workers.delete(handler); 25 | worker.terminate(); 26 | console.log(''); 27 | await sleep(500); 28 | }; 29 | 30 | const test = async handler => { 31 | let [worker, queue] = Workers.get(handler) || create(handler); 32 | 33 | await queue.promise; 34 | queue = Promise.withResolvers(); 35 | handler.resolve = queue.resolve; 36 | worker.addEventListener('message', handler); 37 | 38 | console.time(handler.name); 39 | worker.postMessage('run'); 40 | 41 | await queue.promise; 42 | worker.removeEventListener('message', handler); 43 | // worker.terminate(); 44 | }; 45 | 46 | const coincident = { 47 | resolve: null, 48 | name: 'coincident', 49 | url: 'coincident/roundtrip.js', 50 | track: new Map, 51 | encoder: new TextEncoder, 52 | handleEvent({ data: [ACTION, sab, id] }) { 53 | if (ACTION === 'length') { 54 | const encoded = this.encoder.encode(stringify(data)); 55 | this.track.set(id, encoded); 56 | const i32a = new Int32Array(sab); 57 | i32a[0] = 1; 58 | i32a[1] = encoded.length; 59 | Atomics.notify(i32a, 0); 60 | } 61 | else if (ACTION === 'encode') { 62 | const encoded = this.track.get(id); 63 | this.track.delete(id); 64 | new Uint8Array(sab).set(encoded); 65 | Atomics.notify(new Int32Array(sab), 0); 66 | } 67 | else if (ACTION === 'verify') { 68 | console.timeEnd(this.name); 69 | verify(sab); 70 | this.resolve(); 71 | } 72 | } 73 | }; 74 | 75 | for (let i = 0; i < RUNS; i++) await test(coincident); 76 | await terminate(coincident); 77 | 78 | const ungap = { 79 | resolve: null, 80 | name: 'structured-clone/json', 81 | url: 'ungap/roundtrip.js', 82 | encoder: new TextEncoder, 83 | handleEvent({ data: [ACTION, sab] }) { 84 | if (ACTION === 'encode') { 85 | const encoded = this.encoder.encode(stringify(data)); 86 | const { length } = encoded; 87 | let i = length + 4; 88 | while (i % 4) i++; 89 | sab.grow(i); 90 | new Uint8Array(sab).set(encoded, 4); 91 | const i32a = new Int32Array(sab); 92 | i32a[0] = length; 93 | Atomics.notify(i32a, 0); 94 | } 95 | else if (ACTION === 'verify') { 96 | console.timeEnd(this.name); 97 | verify(sab); 98 | this.resolve(); 99 | } 100 | } 101 | }; 102 | 103 | for (let i = 0; i < RUNS; i++) await test(ungap); 104 | await terminate(ungap); 105 | 106 | const bufferedTwice = { 107 | resolve: null, 108 | name: 'buffered-clone-double', 109 | url: 'buffered/double-roundtrip.js', 110 | track: new Map, 111 | handleEvent({ data: [ACTION, sab, id] }) { 112 | if (ACTION === 'length') { 113 | const encoded = encode(data); 114 | this.track.set(id, encoded); 115 | const i32a = new Int32Array(sab); 116 | i32a[0] = 1; 117 | i32a[1] = encoded.length; 118 | Atomics.notify(i32a, 0); 119 | } 120 | else if (ACTION === 'encode') { 121 | const encoded = this.track.get(id); 122 | this.track.delete(id); 123 | new Uint8Array(sab).set(encoded); 124 | Atomics.notify(new Int32Array(sab), 0); 125 | } 126 | else if (ACTION === 'verify') { 127 | console.timeEnd(this.name); 128 | verify(sab); 129 | this.resolve(); 130 | } 131 | } 132 | }; 133 | 134 | for (let i = 0; i < RUNS; i++) await test(bufferedTwice); 135 | await terminate(bufferedTwice); 136 | 137 | const buffered = { 138 | resolve: null, 139 | name: 'buffered-clone', 140 | url: 'buffered/roundtrip.js', 141 | handleEvent({ data: [ACTION, sab] }) { 142 | if (ACTION === 'encode') { 143 | const encoded = encode(data); 144 | const i32a = new Int32Array(sab); 145 | let { length } = encoded; 146 | if (length < 1) i32a[0] = -1; 147 | else if (length < 2) i32a[0] = 1; 148 | else { 149 | while (length % 4) length++; 150 | sab.grow(length); 151 | new Uint8Array(sab).set(encoded); 152 | } 153 | Atomics.notify(i32a, 0); 154 | } 155 | else if (ACTION === 'verify') { 156 | console.timeEnd(this.name); 157 | verify(sab); 158 | this.resolve(); 159 | } 160 | } 161 | }; 162 | 163 | for (let i = 0; i < RUNS; i++) await test(buffered); 164 | await terminate(buffered); 165 | 166 | document.body.textContent = '✅ Done - see devtools console for results'; 167 | -------------------------------------------------------------------------------- /test/cover_old.js: -------------------------------------------------------------------------------- 1 | import { data, verify } from './data_old.js'; 2 | import { encode, decode } from '../src/index.js'; 3 | 4 | const convert = value => decode(encode(value)); 5 | 6 | const assert = (result, expected) => { 7 | if (!Object.is(result, expected)) { 8 | console.log({ result, expected }); 9 | throw new Error(`Unexpected result`); 10 | } 11 | }; 12 | 13 | console.time('structuredClone'); 14 | const structured = structuredClone(data); 15 | console.timeEnd('structuredClone'); 16 | verify(structured); 17 | 18 | for (let i = 0; i < 10; i++) { 19 | if (i > 8) console.time('complex data via array'); 20 | const converted = convert(data); 21 | if (i > 8) console.timeEnd('complex data via array'); 22 | verify(converted); 23 | 24 | if (i > 8) console.time('complex data via buffer'); 25 | const encodedBuffer = encode(data, { resizable: true }); 26 | const convertedViaBuffer = decode(encodedBuffer); 27 | if (i > 8) console.timeEnd('complex data via buffer'); 28 | verify(convertedViaBuffer); 29 | 30 | if (i > 8) console.time('complex data via pre-allocated buffer'); 31 | const buffer = new ArrayBuffer(encodedBuffer.length); 32 | const allocatedBuffer = decode(encode(data, { buffer })); 33 | if (i > 8) console.timeEnd('complex data via pre-allocated buffer'); 34 | verify(allocatedBuffer); 35 | } 36 | 37 | const length3 = 'a'.repeat(1 << 16); 38 | assert(convert(length3), length3); 39 | 40 | const length4 = 'a'.repeat(1 << 24); 41 | assert(convert(length4), length4); 42 | 43 | assert(convert(NaN), null); 44 | // console.log(convert(NaN), convert([Infinity]), convert({a: -Infinity})); 45 | 46 | assert(JSON.stringify(convert({})), '{}'); 47 | assert(convert(''), ''); 48 | 49 | class Random { 50 | constructor() { 51 | this.ok = true; 52 | } 53 | } 54 | 55 | assert(JSON.stringify(convert(new Random)), '{"ok":true}'); 56 | 57 | try { 58 | decode(new Uint8Array(['!'.charCodeAt(0), 0])); 59 | throw new Error('unknown types should not decode'); 60 | } 61 | catch (OK) {} 62 | 63 | assert( 64 | JSON.stringify(convert([1, () => {}, 2, Symbol(), 3, void 0, 4])), 65 | JSON.stringify([1, () => {}, 2, Symbol(), 3, void 0, 4]) 66 | ); 67 | 68 | assert(convert(true), true); 69 | assert(convert(false), false); 70 | 71 | // Options 72 | const source = ['a', 'a']; 73 | source.unshift(source); 74 | let all = encode(source, { recursion: 'all' }); 75 | let some = encode(source, { recursion: 'some' }); 76 | 77 | try { 78 | encode(source, { recursion: 'none' }); 79 | throw new Error('Unexpected encoding'); 80 | } 81 | catch (OK) {} 82 | 83 | assert(all.join(',') !== some.join(','), true); 84 | 85 | try { 86 | decode(all, { recursion: 'none' }); 87 | throw new Error('recursion should fail'); 88 | } 89 | catch ({ message }) { 90 | assert(message, 'Unexpected Recursion @ 3'); 91 | } 92 | 93 | try { 94 | decode(all, { recursion: 'some' }); 95 | throw new Error('recursion should fail'); 96 | } 97 | catch ({ message }) { 98 | assert(message, 'Unexpected Recursion @ 10'); 99 | } 100 | 101 | assert(decode(some, { recursion: 'some' }).join(','), [[],'a', 'a'].join(',')); 102 | 103 | encode(new Uint8Array(1 << 0).buffer); 104 | encode(new Uint8Array(1 << 8).buffer); 105 | encode(new Uint8Array(1 << 16).buffer); 106 | encode(new Uint8Array(1 << 24).buffer); 107 | 108 | // assert(decode(new Uint8Array([110, 1, 4, 43, 49, 101, 50])), 1e2); 109 | // assert(decode(new Uint8Array([110, 1, 4, 43, 49, 69, 50])), 1E2); 110 | 111 | encode(() => {}); 112 | encode(new DataView(new ArrayBuffer(0)), { resizable: true }); 113 | 114 | // test empty ascii string 115 | decode(encode(['a', /no-flags/, 'b'])); 116 | 117 | decode(encode([1, 1n, 1], { recursion: 'some' }), { recursion: 'some' }); 118 | 119 | // test fallback for Error and TypedArray 120 | const name = [...encode('Unknown')]; 121 | // decode(new Uint8Array([101, ...name, ...encode('message')])); 122 | // decode(new Uint8Array([84, ...name, ...encode(new ArrayBuffer(0))])); 123 | 124 | // toBufferedClone 125 | const toBufferedClone = Symbol.for('buffered-clone'); 126 | 127 | let invokes = 0; 128 | class Recursive { 129 | [toBufferedClone]() { 130 | invokes++; 131 | const object = { seppuku: this }; 132 | object.recursive = object; 133 | return object; 134 | } 135 | } 136 | 137 | let ref = new Recursive; 138 | let arr = decode(encode([ref, ref])); 139 | assert(invokes, 1); 140 | assert(arr.length, 2); 141 | assert(arr[0], arr[1]); 142 | assert(arr[0].recursive, arr[1]); 143 | assert(arr[0].seppuku, arr[0]); 144 | 145 | invokes = 0; 146 | class NotRecursive { 147 | [toBufferedClone]() { 148 | invokes++; 149 | return { invokes }; 150 | } 151 | } 152 | 153 | ref = new NotRecursive; 154 | arr = decode(encode([ref, ref], { recursion: 'none' })); 155 | assert(invokes, 2); 156 | assert(arr.length, 2); 157 | assert(arr[0].invokes, 1); 158 | assert(arr[1].invokes, 2); 159 | 160 | assert(null, decode(encode({ [toBufferedClone]() { return null } }))); 161 | 162 | invokes = 0; 163 | class BadRecursion { 164 | [toBufferedClone]() { 165 | invokes++; 166 | return null; 167 | } 168 | } 169 | 170 | ref = new BadRecursion; 171 | arr = decode(encode([ref, ref])); 172 | assert(invokes, 1); 173 | assert(arr.length, 2); 174 | assert(arr.every(v => v === null), true); 175 | 176 | invokes = 0; 177 | class SelfRecursion { 178 | [toBufferedClone]() { 179 | invokes++; 180 | return this; 181 | } 182 | } 183 | 184 | ref = new SelfRecursion; 185 | arr = decode(encode([ref, ref])); 186 | assert(invokes, 1); 187 | assert(arr.length, 2); 188 | assert(arr.every(v => v === null), true); 189 | 190 | invokes = 0; 191 | class DifferentRecursion { 192 | [toBufferedClone]() { 193 | invokes++; 194 | return Math.random(); 195 | } 196 | } 197 | 198 | ref = new DifferentRecursion; 199 | arr = decode(encode([ref, ref, 'ok'], { recursion: 'some' })); 200 | assert(invokes, 1); 201 | assert(arr.length, 3); 202 | assert(arr[0], arr[1]); 203 | assert(arr[2], 'ok'); 204 | 205 | assert(decode(encode(-1)), -1); 206 | assert(decode(encode(-128)), -128); 207 | assert(decode(encode(-70000)), -70000); 208 | assert(decode(encode(-3000000)), -3000000); 209 | assert(decode(encode(-4294967296 / 2)), -4294967296 / 2); 210 | assert(decode(encode(4294967296)), 4294967296); 211 | assert(decode(encode(-1n)), -1n); 212 | assert(decode(encode(1n)), 1n); 213 | 214 | for (const Class of [ 215 | Int8Array, 216 | Int16Array, 217 | Int32Array, 218 | Uint8Array, 219 | Uint16Array, 220 | Uint32Array, 221 | Float32Array, 222 | Float64Array, 223 | BigInt64Array, 224 | BigUint64Array, 225 | ]) { 226 | assert(decode(encode(new Class(1))).constructor, Class); 227 | } 228 | 229 | assert(decode(encode(['a', 'a'], { resizable: true, recursion: 'none' })).join(','), 'a,a'); 230 | 231 | import * as number from '../src/number.js'; 232 | import { U16, U32, F32 } from '../src/constants.js'; 233 | 234 | assert(decode(new Uint8Array([U16, ...number.u16.encode(123)])), 123); 235 | assert(decode(new Uint8Array([U32, ...number.u32.encode(123)])), 123); 236 | assert(decode(new Uint8Array([F32, ...number.f32.encode(123)])), 123); 237 | 238 | assert(number.u16.encode(123).length, 2); 239 | assert(number.u32.encode(123).length, 4); 240 | assert(number.f32.encode(123).length, 4); 241 | 242 | class NotError extends Error { 243 | name = 'NotError'; 244 | } 245 | 246 | assert(decode(encode(new NotError('because'))).message, 'because'); 247 | 248 | assert(decode([]), void 0); 249 | assert(decode(encode('')), ''); 250 | assert(decode(encode(['a', '', '', 'b'])).join('-'), 'a---b'); 251 | assert(decode(encode(1.2345678901234567)), 1.2345678901234567); 252 | -------------------------------------------------------------------------------- /test/cover.js: -------------------------------------------------------------------------------- 1 | import { data, verify } from './data.js'; 2 | import BufferedClone from '../src/index.js'; 3 | import { Encoder } from '../src/encoder.js'; 4 | import { Decoder } from '../src/decoder.js'; 5 | 6 | const { encode, decode } = new BufferedClone; 7 | const encoder = new Encoder({ 8 | useFloat32: true, 9 | circular: false, 10 | useUTF16: true, 11 | mirrored: ['a', 'b', 'c'] 12 | }); 13 | const decoder = new Decoder({ 14 | useFloat32: true, 15 | circular: false, 16 | useUTF16: true, 17 | mirrored: ['a', 'b', 'c'] 18 | }); 19 | 20 | const convert = value => decode(encode(value)); 21 | 22 | const assert = (result, expected) => { 23 | if (!Object.is(result, expected)) { 24 | console.log({ expected, result }); 25 | throw new Error(`Unexpected result`); 26 | } 27 | }; 28 | 29 | assert(decoder.decode(encoder.encode(['a','b','c'])).join(','), 'a,b,c'); 30 | assert(decoder.decode(encoder.encode({symbol: Symbol.iterator})).symbol, Symbol.iterator); 31 | assert(decoder.decode(encoder.encode({symbol: Symbol.for('test')})).symbol, Symbol.for('test')); 32 | assert(decoder.decode(encoder.encode(-0x80000001)), -0x80000001); 33 | assert(decoder.decode(encoder.encode(-0x8000)), -0x8000); 34 | assert(decoder.decode(encoder.encode("💩")), "💩"); 35 | assert(decoder.decode(encoder.encode("💩".repeat(0x10000))), "💩".repeat(0x10000)); 36 | assert(decoder.decode(encoder.encode(1.2)).toFixed(2), (1.2).toFixed(2)); 37 | 38 | const d = new Date; 39 | assert(decoder.decode(encoder.encode(d)).getTime(), d.getTime()); 40 | 41 | console.time('structuredClone'); 42 | const structured = structuredClone(data); 43 | console.timeEnd('structuredClone'); 44 | verify(structured); 45 | 46 | for (let i = 0; i < 10; i++) { 47 | if (i > 8) console.time('complex data via array'); 48 | const converted = convert(data); 49 | if (i > 8) console.timeEnd('complex data via array'); 50 | verify(converted); 51 | 52 | if (i > 8) console.time('complex data via buffer'); 53 | const encodedBuffer = encode(data); 54 | const convertedViaBuffer = decode(encodedBuffer); 55 | if (i > 8) console.timeEnd('complex data via buffer'); 56 | verify(convertedViaBuffer); 57 | 58 | if (i > 8) console.time('complex data via pre-allocated buffer'); 59 | const buffer = new ArrayBuffer(encodedBuffer.length); 60 | encode(data, buffer); 61 | const allocatedBuffer = decode(new Uint8Array(buffer)); 62 | if (i > 8) console.timeEnd('complex data via pre-allocated buffer'); 63 | verify(allocatedBuffer); 64 | } 65 | 66 | const length3 = 'a'.repeat(1 << 16); 67 | assert(convert(length3), length3); 68 | 69 | const length4 = 'a'.repeat(1 << 24); 70 | assert(convert(length4), length4); 71 | 72 | assert(convert(NaN), null); 73 | // console.log(convert(NaN), convert([Infinity]), convert({a: -Infinity})); 74 | 75 | assert(JSON.stringify(convert({})), '{}'); 76 | assert(convert(''), ''); 77 | 78 | class Random { 79 | constructor() { 80 | this.ok = true; 81 | } 82 | } 83 | 84 | assert(JSON.stringify(convert(new Random)), '{"ok":true}'); 85 | 86 | try { 87 | decode(new Uint8Array(['!'.charCodeAt(0), 0])); 88 | throw new Error('unknown types should not decode'); 89 | } 90 | catch (OK) {} 91 | 92 | // assert( 93 | // JSON.stringify(convert([1, () => {}, 2, Symbol(), 3, void 0, 4])), 94 | // JSON.stringify([1, () => {}, 2, Symbol(), 3, void 0, 4]) 95 | // ); 96 | 97 | assert(convert(true), true); 98 | assert(convert(false), false); 99 | 100 | // Options 101 | // const source = ['a', 'a']; 102 | // source.unshift(source); 103 | // let all = encode(source, { recursion: 'all' }); 104 | // let some = encode(source, { recursion: 'some' }); 105 | 106 | // try { 107 | // encode(source, { recursion: 'none' }); 108 | // throw new Error('Unexpected encoding'); 109 | // } 110 | // catch (OK) {} 111 | // 112 | // assert(all.join(',') !== some.join(','), true); 113 | 114 | // try { 115 | // decode(all, { recursion: 'none' }); 116 | // throw new Error('recursion should fail'); 117 | // } 118 | // catch ({ message }) { 119 | // assert(message, 'Unexpected Recursion @ 3'); 120 | // } 121 | 122 | // try { 123 | // decode(all, { recursion: 'some' }); 124 | // throw new Error('recursion should fail'); 125 | // } 126 | // catch ({ message }) { 127 | // assert(message, 'Unexpected Recursion @ 10'); 128 | // } 129 | 130 | // assert(decode(some, { recursion: 'some' }).join(','), [[],'a', 'a'].join(',')); 131 | 132 | encode(new Uint8Array(1 << 0).buffer); 133 | encode(new Uint8Array(1 << 8).buffer); 134 | encode(new Uint8Array(1 << 16).buffer); 135 | encode(new Uint8Array(1 << 24).buffer); 136 | // encode(new Uint8Array(1 << 30).buffer); 137 | 138 | // assert(decode(new Uint8Array([110, 1, 4, 43, 49, 101, 50])), 1e2); 139 | // assert(decode(new Uint8Array([110, 1, 4, 43, 49, 69, 50])), 1E2); 140 | 141 | encode(() => {}); 142 | encode(new DataView(new ArrayBuffer(0))); 143 | 144 | // test empty ascii string 145 | decode(encode(['a', /no-flags/, 'b'])); 146 | 147 | decode(encode([1, 1n, 1])); 148 | 149 | // test fallback for Error and TypedArray 150 | const name = [...encode('Unknown')]; 151 | // decode(new Uint8Array([101, ...name, ...encode('message')])); 152 | // decode(new Uint8Array([84, ...name, ...encode(new ArrayBuffer(0))])); 153 | 154 | // toBufferedClone 155 | const toBufferedClone = Symbol.for('buffered-clone'); 156 | 157 | let invokes = 0; 158 | class Recursive { 159 | toJSON() { 160 | invokes++; 161 | const object = { seppuku: this }; 162 | object.recursive = object; 163 | return object; 164 | } 165 | } 166 | 167 | let ref = new Recursive; 168 | let arr = decode(encode([ref, ref])); 169 | assert(invokes, 1); 170 | assert(arr.length, 2); 171 | assert(arr[0], arr[1]); 172 | assert(arr[0].recursive, arr[1]); 173 | assert(arr[0].seppuku, arr[0]); 174 | 175 | invokes = 0; 176 | class NotRecursive { 177 | toJSON() { 178 | invokes++; 179 | return { invokes }; 180 | } 181 | } 182 | 183 | ref = new NotRecursive; 184 | arr = decode(encode([ref, ref])); 185 | assert(invokes, 1); 186 | assert(arr.length, 2); 187 | assert(arr[0].invokes, 1); 188 | assert(arr[1].invokes, 1); 189 | 190 | assert(null, decode(encode({ toJSON() { return null } }))); 191 | 192 | invokes = 0; 193 | class BadRecursion { 194 | toJSON() { 195 | invokes++; 196 | return null; 197 | } 198 | } 199 | 200 | ref = new BadRecursion; 201 | arr = decode(encode([ref, ref])); 202 | assert(invokes, 1); 203 | assert(arr.length, 2); 204 | assert(arr.every(v => v === null), true); 205 | 206 | invokes = 0; 207 | class SelfRecursion { 208 | toJSON() { 209 | invokes++; 210 | return this; 211 | } 212 | } 213 | 214 | ref = new SelfRecursion; 215 | arr = decode(encode([ref, ref])); 216 | assert(invokes, 1); 217 | assert(arr.length, 2); 218 | assert(arr.every(v => v === null), true); 219 | 220 | invokes = 0; 221 | class DifferentRecursion { 222 | toJSON() { 223 | invokes++; 224 | return Math.random(); 225 | } 226 | } 227 | 228 | ref = new DifferentRecursion; 229 | arr = decode(encode([ref, ref, 'ok'])); 230 | assert(invokes, 1); 231 | assert(arr.length, 3); 232 | assert(arr[0], arr[1]); 233 | assert(arr[2], 'ok'); 234 | 235 | assert(decode(encode(-1)), -1); 236 | assert(decode(encode(-128)), -128); 237 | assert(decode(encode(-70000)), -70000); 238 | assert(decode(encode(-3000000)), -3000000); 239 | assert(decode(encode(-4294967296 / 2)), -4294967296 / 2); 240 | assert(decode(encode(4294967296)), 4294967296); 241 | assert(decode(encode(-1n)), -1n); 242 | assert(decode(encode(1n)), 1n); 243 | 244 | for (const Class of [ 245 | Int8Array, 246 | Int16Array, 247 | Int32Array, 248 | Uint8Array, 249 | Uint16Array, 250 | Uint32Array, 251 | Float32Array, 252 | Float64Array, 253 | BigInt64Array, 254 | BigUint64Array, 255 | ]) { 256 | assert(decode(encode(new Class(1))).constructor, Class); 257 | } 258 | 259 | assert(decode(encode(['a', 'a'])).join(','), 'a,a'); 260 | 261 | // import * as number from '../src/number.js'; 262 | // import { U16, U32, F32 } from '../src/constants.js'; 263 | 264 | // assert(decode(new Uint8Array([U16, ...number.u16.encode(123)])), 123); 265 | // assert(decode(new Uint8Array([U32, ...number.u32.encode(123)])), 123); 266 | // assert(decode(new Uint8Array([F32, ...number.f32.encode(123)])), 123); 267 | 268 | // assert(number.u16.encode(123).length, 2); 269 | // assert(number.u32.encode(123).length, 4); 270 | // assert(number.f32.encode(123).length, 4); 271 | 272 | // class NotError extends Error { 273 | // name = 'NotError'; 274 | // } 275 | 276 | // assert(decode(encode(new NotError('because'))).message, 'because'); 277 | 278 | // assert(decode([]), void 0); 279 | // assert(decode(encode('')), ''); 280 | // assert(decode(encode(['a', '', '', 'b'])).join('-'), 'a---b'); 281 | // assert(decode(encode(1.2345678901234567)), 1.2345678901234567); 282 | -------------------------------------------------------------------------------- /old/decode.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { 4 | // JSON 5 | ARRAY, 6 | OBJECT, 7 | STRING, 8 | TRUE, 9 | FALSE, 10 | NULL, 11 | 12 | // numbers 13 | I8A, I8, 14 | U8A, U8, 15 | I16A, I16, 16 | U16A, U16, 17 | F16A, F16, 18 | I32A, I32, 19 | F32A, F32, 20 | U32A, U32, 21 | I64A, I64, 22 | F64A, F64, 23 | U64A, U64, 24 | 25 | // JS types 26 | BUFFER, 27 | RECURSIVE, 28 | ERROR, 29 | REGEXP, 30 | SET, 31 | MAP, 32 | DATE, 33 | DATAVIEW, 34 | IMAGEDATA, 35 | } from './constants.js'; 36 | 37 | import * as number from './number.js'; 38 | 39 | /** 40 | * @typedef {object} Options 41 | * @prop {'all'|'some'|'none'} recursion With `all`, the default, everything recursive will be tracked. With `some`, all primitives get ignored or fail if found as recursive. With `none`, no recursion is ever tracked and an error is thrown when any recursive data is found. 42 | */ 43 | 44 | const { fromCharCode } = String; 45 | const decoder = new TextDecoder; 46 | 47 | /** 48 | * @param {number} length 49 | * @returns 50 | */ 51 | const alloc = length => new Array(length); 52 | 53 | /** 54 | * @param {number} at 55 | */ 56 | const throwOnRecursion = at => { 57 | M.clear(); 58 | throw new SyntaxError(`Unexpected Recursion @ ${at}`); 59 | }; 60 | 61 | /** 62 | * @param {M|V} map 63 | * @param {number} as 64 | * @param {any} value 65 | * @returns 66 | */ 67 | const track = (map, as, value) => { 68 | map.set(as, value); 69 | return value; 70 | }; 71 | 72 | class Decoder { 73 | /** 74 | * @param {Uint8Array} a 75 | * @param {M|V} m 76 | * @param {boolean} p 77 | */ 78 | constructor(m, a, p) { 79 | this.i = 0; 80 | this.m = m; 81 | this.a = a; 82 | this.p = p; 83 | } 84 | 85 | /** 86 | * @param {any[]} value 87 | * @returns 88 | */ 89 | array(value) { 90 | for (let i = 0; i < value.length; i++) 91 | value[i] = this.decode(); 92 | return value; 93 | } 94 | 95 | /** 96 | * @returns {string} 97 | */ 98 | ascii() { 99 | const length = this.length(); 100 | if (length) { 101 | const i = this.i; 102 | const codes = this.a.subarray(i, (this.i += length)); 103 | return fromCharCode.apply(null, codes); 104 | } 105 | return ''; 106 | } 107 | 108 | buffer() { 109 | const length = this.length(); 110 | const start = this.i; 111 | const end = (this.i += length); 112 | return this.a.buffer.slice(start, end); 113 | } 114 | 115 | decode() { 116 | const as = this.i++; 117 | switch (this.a[as]) { 118 | case RECURSIVE: return this.m.get(this.length()) ?? throwOnRecursion(as); 119 | // JSON arrays / objects 120 | case OBJECT: return this.object(track(this.m, as, {})); 121 | case ARRAY: return this.array(track(this.m, as, alloc(this.length()))); 122 | // strings 123 | // case ASCII: return this.string(as, true); 124 | case STRING: return this.string(as); 125 | // numbers 126 | case I8: return this.number(as, number.i8); 127 | case U8: return this.number(as, number.u8); 128 | case I16: return this.number(as, number.i16); 129 | case U16: return this.number(as, number.u16); 130 | case I32: return this.number(as, number.i32); 131 | case F32: return this.number(as, number.f32); 132 | case U32: return this.number(as, number.u32); 133 | case I64: return this.number(as, number.i64); 134 | case F64: return this.number(as, number.f64); 135 | case U64: return this.number(as, number.u64); 136 | // typed / dataview 137 | case I8A: return track(this.m, as, new Int8Array(this.decode())); 138 | case U8A: return track(this.m, as, new Uint8Array(this.decode())); 139 | case I16A: return track(this.m, as, new Int16Array(this.decode())); 140 | case U16A: return track(this.m, as, new Uint16Array(this.decode())); 141 | case I32A: return track(this.m, as, new Int32Array(this.decode())); 142 | case F32A: return track(this.m, as, new Float32Array(this.decode())); 143 | case U32A: return track(this.m, as, new Uint32Array(this.decode())); 144 | case I64A: return track(this.m, as, new BigInt64Array(this.decode())); 145 | case F64A: return track(this.m, as, new Float64Array(this.decode())); 146 | case U64A: return track(this.m, as, new BigUint64Array(this.decode())); 147 | case DATAVIEW: return track(this.m, as, new DataView(this.decode())); 148 | /* c8 ignore next */ 149 | case IMAGEDATA: return this.imageData(as); 150 | // boolean 151 | case TRUE: return true; 152 | case FALSE: return false; 153 | // null 154 | case NULL: return null; 155 | // other types 156 | case DATE: return track(this.m, as, new Date(this.ascii())); 157 | case MAP: return this.map(track(this.m, as, new Map)); 158 | case SET: return this.set(track(this.m, as, new Set)); 159 | case BUFFER: return track(this.m, as, this.buffer()); 160 | case REGEXP: return track(this.m, as, this.regexp()); 161 | case ERROR: return track(this.m, as, this.error()); 162 | /* c8 ignore next 3 */ 163 | case F16: return this.number(as, number.f16); 164 | //@ts-ignore 165 | case F16A: return track(this.m, as, new Float16Array(this.decode())); 166 | default: { 167 | M.clear(); 168 | const type = fromCharCode(this.a[as]); 169 | throw new TypeError(`Unable to decode type: ${type}`); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * @returns {Error} 176 | */ 177 | error() { 178 | this.i++; 179 | const name = this.ascii(); 180 | const Class = globalThis[name] || Error; 181 | return new Class(this.decode()); 182 | } 183 | 184 | /* c8 ignore next 11 */ 185 | /** 186 | * @param {number} as 187 | * @returns {ImageData} 188 | */ 189 | imageData(as) { 190 | const [{ buffer }, ...rest] = this.array(alloc(this.length())); 191 | //@ts-ignore 192 | const value = new ImageData(new Uint8ClampedArray(buffer), ...rest); 193 | this.m.set(as, value); 194 | return value; 195 | } 196 | 197 | /** 198 | * @returns {number} 199 | */ 200 | length() { 201 | const { a, i } = this; 202 | switch (a[i]) { 203 | case U8: { 204 | this.i += 2; 205 | return a[i + 1]; 206 | }; 207 | case U16: { 208 | const value = a.subarray(i + 1, this.i += 3); 209 | return /** @type {number} */(number.u16.decode(value)); 210 | } 211 | default: { 212 | const value = a.subarray(i + 1, this.i += 5); 213 | return /** @type {number} */(number.u32.decode(value)); 214 | } 215 | } 216 | } 217 | 218 | /** 219 | * @param {Map} value 220 | * @returns 221 | */ 222 | map(value) { 223 | for (let i = 0, length = this.length(); i < length; i += 2) 224 | value.set(this.decode(), this.decode()); 225 | return value; 226 | } 227 | 228 | /** 229 | * @param {number} as 230 | * @param {import("./number.js").Number} decoder 231 | * @returns {number|bigint} 232 | */ 233 | number(as, decoder) { 234 | let { i, m, a } = this; 235 | this.i += decoder.length; 236 | const value = decoder === number.u8 ? a[i] : decoder.decode(a.subarray(i, this.i)); 237 | return this.p ? track(m, as, value) : value; 238 | } 239 | 240 | /** 241 | * @param {object} value 242 | * @returns {object} 243 | */ 244 | object(value) { 245 | for (let i = 0, length = this.length(); i < length; i += 2) 246 | value[this.decode()] = this.decode(); 247 | return value; 248 | } 249 | 250 | /** 251 | * @returns {RegExp} 252 | */ 253 | regexp() { 254 | const source = this.decode(); 255 | this.i++; 256 | const flags = this.ascii(); 257 | return new RegExp(source, flags); 258 | } 259 | 260 | /** 261 | * @param {Set} value 262 | * @returns 263 | */ 264 | set(value) { 265 | for (let i = 0, length = this.length(); i < length; i++) 266 | value.add(this.decode()); 267 | return value; 268 | } 269 | 270 | /** 271 | * @param {number} as 272 | * @returns {string} 273 | */ 274 | string(as) { 275 | const length = this.length(); 276 | if (length) { 277 | const start = this.i; 278 | const end = (this.i += length); 279 | // ⚠️ this cannot be a subarray because TextDecoder will 280 | // complain if the view's buffer is a SharedArrayBuffer 281 | // or, probably, also if it was a resizable ArrayBuffer 282 | const value = decoder.decode(this.a.slice(start, end)); 283 | return this.p ? track(this.m, as, value) : value; 284 | } 285 | return ''; 286 | } 287 | } 288 | 289 | const V = { 290 | /** 291 | * @param {number} i 292 | */ 293 | get(i) {}, 294 | 295 | /** 296 | * @param {number} i 297 | * @param {any} value 298 | */ 299 | set(i, value) {}, 300 | }; 301 | 302 | // ⚠️ in encode it was not possible to 303 | // encode while encoding due shared Map 304 | // in here there is not such thing because 305 | // nothing can decode while decoding *but* 306 | // if one day a fromBufferedClone thing happens 307 | // and it happens during decoding, this shared 308 | // map idea becomes more dangerous than useful. 309 | /** @typedef {Map} */ 310 | const M = new Map; 311 | 312 | /** 313 | * @param {Uint8Array} ui8a 314 | * @param {Options?} options 315 | * @returns 316 | */ 317 | export default (ui8a, options) => { 318 | const r = options?.recursion; 319 | const value = ui8a.length ? new Decoder(r === 'none' ? V : M, ui8a, r !== 'some').decode() : void 0; 320 | M.clear(); 321 | return value; 322 | }; 323 | -------------------------------------------------------------------------------- /src/decoder.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import defaultOptions from './options.js'; 4 | import views from './views.js'; 5 | 6 | import { 7 | MAX_ARGS, 8 | DataView, 9 | Date, 10 | Error, 11 | ImageData, 12 | Map, 13 | Object, 14 | RegExp, 15 | Set, 16 | String, 17 | } from './globals.js'; 18 | 19 | const { create } = Object; 20 | const { fromCharCode } = String; 21 | 22 | const Null = () => null; 23 | const False = () => false; 24 | const True = () => true; 25 | const Void = () => {}; 26 | 27 | /** 28 | * @param {number[] | ArrayBufferView} codes 29 | * @returns 30 | */ 31 | const asUTF16String = codes => fromCharCode.apply(null, codes); 32 | 33 | const td = new TextDecoder('utf-8', { fatal: true }); 34 | 35 | export const decoder = ({ 36 | littleEndian = defaultOptions.littleEndian, 37 | circular = defaultOptions.circular, 38 | mirrored = defaultOptions.mirrored, 39 | } = defaultOptions) => { 40 | let i = 0; 41 | 42 | /** 43 | * object 44 | * @param {DataView} data 45 | * @param {Uint8Array} view 46 | * @param {object} cache 47 | * @returns 48 | */ 49 | const builtin = (data, view, cache) => { 50 | //@ts-ignore 51 | return builtins[~data.getInt8(i++)](data, view, cache); 52 | } 53 | 54 | const builtins = [ 55 | // null 56 | Null, 57 | 58 | // false 59 | False, 60 | 61 | // true 62 | True, 63 | 64 | /** 65 | * circular 66 | * @param {DataView} data 67 | * @param {Uint8Array} view 68 | * @param {object} cache 69 | * @returns{any} 70 | */ 71 | (data, view, cache) => cache[builtin(data, view, cache)], 72 | 73 | /** 74 | * ascii 75 | * @param {DataView} data 76 | * @param {Uint8Array} view 77 | * @param {object} cache 78 | * @returns 79 | */ 80 | (data, view, cache) => { 81 | const index = i; 82 | const length = builtin(data, view, cache); 83 | let value = ''; 84 | if (length) { 85 | value = asUTF16String(view.subarray(i, i + length)); 86 | i += length; 87 | if (circular) cache[index - 1] = value; 88 | } 89 | return value; 90 | }, 91 | 92 | /** 93 | * utf8 string 94 | * @param {DataView} data 95 | * @param {Uint8Array} view 96 | * @param {object} cache 97 | * @returns 98 | */ 99 | (data, view, cache) => { 100 | const index = i; 101 | const length = builtin(data, view, cache); 102 | const value = td.decode(view.slice(i, i + length)); 103 | i += length; 104 | if (circular) cache[index - 1] = value; 105 | return value; 106 | }, 107 | 108 | /** 109 | * utf16 string 110 | * @param {DataView} data 111 | * @param {Uint8Array} view 112 | * @param {object} cache 113 | * @returns 114 | */ 115 | (data, view, cache) => { 116 | const index = i; 117 | const length = builtin(data, view, cache); 118 | /** @type {number[]} */ 119 | const codes = []; 120 | let value = ''; 121 | for (let j = 0; j < length; j++) { 122 | if (0 < j && (j % MAX_ARGS) === 0) 123 | value += asUTF16String(codes.splice(0)); 124 | codes.push(data.getUint16(i, littleEndian)); 125 | i += 2; 126 | } 127 | value += asUTF16String(codes.splice(0)); 128 | /* c8 ignore next */ 129 | if (circular) cache[index - 1] = value; 130 | return value; 131 | }, 132 | 133 | // leave room for other string variants, if ever 134 | Void(), 135 | Void(), 136 | 137 | /** 138 | * buffer 139 | * @param {DataView} data 140 | * @param {Uint8Array} view 141 | * @param {object} cache 142 | * @returns 143 | */ 144 | (data, view, cache) => { 145 | const index = i; 146 | const length = builtin(data, view, cache); 147 | const value = view.buffer.slice(i, i + length); 148 | if (circular) cache[index - 1] = value; 149 | i += length; 150 | return value; 151 | }, 152 | 153 | /** 154 | * array 155 | * @param {DataView} data 156 | * @param {Uint8Array} view 157 | * @param {object} cache 158 | * @returns 159 | */ 160 | (data, view, cache) => { 161 | const index = i; 162 | const length = builtin(data, view, cache); 163 | const value = Array(length); 164 | if (circular) cache[index - 1] = value; 165 | for (let j = 0; j < length; j++) 166 | value[j] = decode(data, view, cache); 167 | return value; 168 | }, 169 | 170 | /** 171 | * date 172 | * @param {DataView} data 173 | * @param {Uint8Array} view 174 | * @param {object} cache 175 | * @returns 176 | */ 177 | (data, view, cache) => { 178 | const index = i; 179 | const value = new Date(builtin(data, view, cache)); 180 | if (circular) cache[index - 1] = value; 181 | return value; 182 | }, 183 | 184 | /** 185 | * object 186 | * @param {DataView} data 187 | * @param {Uint8Array} view 188 | * @param {object} cache 189 | * @returns 190 | */ 191 | (data, view, cache) => { 192 | const index = i; 193 | const length = builtin(data, view, cache); 194 | const value = {}; 195 | if (circular) cache[index - 1] = value; 196 | for (let j = 0; j < length; j++) 197 | value[builtin(data, view, cache)] = decode(data, view, cache); 198 | return value; 199 | }, 200 | 201 | /** 202 | * symbol - known or public cymbols 203 | * @param {DataView} data 204 | * @param {Uint8Array} view 205 | * @param {object} cache 206 | * @returns{symbol} 207 | */ 208 | (data, view, cache) => { 209 | const value = builtin(data, view, cache); 210 | return Symbol[value] || Symbol.for(value); 211 | }, 212 | 213 | /** 214 | * view 215 | * @param {DataView} data 216 | * @param {Uint8Array} view 217 | * @param {object} cache 218 | * @returns {ArrayBufferView} 219 | */ 220 | (data, view, cache) => { 221 | const index = i; 222 | const Class = views[data.getUint8(i++)]; 223 | //@ts-ignore 224 | const value = new Class(builtin(data, view, cache)); 225 | if (circular) cache[index - 1] = value; 226 | return value; 227 | }, 228 | 229 | /** 230 | * map 231 | * @param {DataView} data 232 | * @param {Uint8Array} view 233 | * @param {object} cache 234 | * @returns 235 | */ 236 | (data, view, cache) => { 237 | const index = i; 238 | const length = builtin(data, view, cache); 239 | const value = new Map; 240 | if (circular) cache[index - 1] = value; 241 | for (let j = 0; j < length; j++) 242 | value.set(decode(data, view, cache), decode(data, view, cache)); 243 | return value; 244 | }, 245 | 246 | /** 247 | * set 248 | * @param {DataView} data 249 | * @param {Uint8Array} view 250 | * @param {object} cache 251 | * @returns 252 | */ 253 | (data, view, cache) => { 254 | const index = i; 255 | const length = builtin(data, view, cache); 256 | const value = new Set; 257 | if (circular) cache[index - 1] = value; 258 | for (let j = 0; j < length; j++) 259 | value.add(decode(data, view, cache)); 260 | return value; 261 | }, 262 | 263 | /** 264 | * regexp 265 | * @param {DataView} data 266 | * @param {Uint8Array} view 267 | * @param {object} cache 268 | * @returns 269 | */ 270 | (data, view, cache) => { 271 | const index = i; 272 | const value = new RegExp( 273 | builtin(data, view, cache), 274 | builtin(data, view, cache) 275 | ); 276 | if (circular) cache[index - 1] = value; 277 | return value; 278 | }, 279 | 280 | /** 281 | * imagedata 282 | * @param {DataView} data 283 | * @param {Uint8Array} view 284 | * @param {object} cache 285 | * @returns 286 | */ 287 | (data, view, cache) => { 288 | /* c8 ignore next 8 */ 289 | const index = i; 290 | const ui8ca = new Uint8ClampedArray(builtin(data, view, cache)); 291 | const width = builtin(data, view, cache); 292 | const height = builtin(data, view, cache); 293 | const options = builtin(data, view, cache); 294 | const value = new ImageData(ui8ca, width, height, options); 295 | if (circular) cache[index - 1] = value; 296 | return value; 297 | }, 298 | 299 | /** 300 | * error 301 | * @param {DataView} data 302 | * @param {Uint8Array} view 303 | * @param {object} cache 304 | * @returns {Error} 305 | */ 306 | (data, view, cache) => { 307 | const index = i; 308 | /* c8 ignore next */ 309 | const Class = globalThis[builtin(data, view, cache)] || Error; 310 | //@ts-ignore 311 | const value = new Class(builtin(data, view, cache)); 312 | if (circular) cache[index - 1] = value; 313 | return value; 314 | }, 315 | ]; 316 | 317 | // leave room for future builtins 318 | for (let j = builtins.length; j < 78; j++) 319 | builtins[j] = Void; 320 | 321 | builtins.push( 322 | /** 323 | * mirrored 324 | * @param {DataView} data 325 | * @param {Uint8Array} view 326 | * @param {object} cache 327 | * @returns 328 | */ 329 | (data, view, cache) => mirrored[builtin(data, view, cache)], 330 | ); 331 | 332 | // reserve final slots for numbers (starts at -80) 333 | builtins.push( 334 | /** 335 | * i8 336 | * @param {DataView} data 337 | * @returns 338 | */ 339 | data => data.getInt8(i++), 340 | 341 | /** 342 | * u8 343 | * @param {DataView} data 344 | * @returns 345 | */ 346 | data => data.getUint8(i++), 347 | 348 | /** 349 | * i16 350 | * @param {DataView} data 351 | * @returns 352 | */ 353 | data => { 354 | const value = data.getInt16(i, littleEndian); 355 | i += 2; 356 | return value; 357 | }, 358 | 359 | /** 360 | * u16 361 | * @param {DataView} data 362 | * @returns 363 | */ 364 | data => { 365 | /* c8 ignore next 3 */ 366 | const value = data.getUint16(i, littleEndian); 367 | i += 2; 368 | return value; 369 | }, 370 | 371 | /** 372 | * f16 373 | * @param {DataView} data 374 | * @returns 375 | */ 376 | data => { 377 | /* c8 ignore next 4 */ 378 | //@ts-ignore 379 | const value = data.getFloat16(i, littleEndian); 380 | i += 2; 381 | return value; 382 | }, 383 | 384 | /** 385 | * i32 386 | * @param {DataView} data 387 | * @returns 388 | */ 389 | data => { 390 | const value = data.getInt32(i, littleEndian); 391 | i += 4; 392 | return value; 393 | }, 394 | 395 | /** 396 | * u32 397 | * @param {DataView} data 398 | * @returns 399 | */ 400 | data => { 401 | const value = data.getUint32(i, littleEndian); 402 | i += 4; 403 | return value; 404 | }, 405 | 406 | /** 407 | * f32 408 | * @param {DataView} data 409 | * @returns 410 | */ 411 | data => { 412 | const value = data.getFloat32(i, littleEndian); 413 | i += 4; 414 | return value; 415 | }, 416 | 417 | /** 418 | * i64 419 | * @param {DataView} data 420 | * @returns 421 | */ 422 | data => { 423 | const value = data.getBigInt64(i, littleEndian); 424 | i += 8; 425 | return value; 426 | }, 427 | 428 | /** 429 | * u64 430 | * @param {DataView} data 431 | * @returns 432 | */ 433 | data => { 434 | const value = data.getBigUint64(i, littleEndian); 435 | i += 8; 436 | return value; 437 | }, 438 | 439 | /** 440 | * f64 441 | * @param {DataView} data 442 | * @returns 443 | */ 444 | data => { 445 | const value = data.getFloat64(i, littleEndian); 446 | i += 8; 447 | return value; 448 | }, 449 | ); 450 | 451 | /** 452 | * @param {DataView} data 453 | * @param {Uint8Array} view 454 | * @param {object} cache 455 | * @returns {any} 456 | */ 457 | const decode = (data, view, cache) => { 458 | const index = data.getInt8(i++); 459 | //@ts-ignore 460 | return index < 0 ? builtins[~index](data, view, cache) : null; 461 | }; 462 | 463 | /** 464 | * @param {Uint8Array} view 465 | * @returns 466 | */ 467 | return view => { 468 | i = 0; 469 | const { buffer, byteOffset } = view; 470 | return decode(new DataView(buffer, byteOffset), view, create(null)); 471 | }; 472 | }; 473 | 474 | export class Decoder { 475 | constructor(options = defaultOptions) { 476 | this.decode = decoder(options); 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /old/encode.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { 4 | // JSON 5 | ARRAY, 6 | OBJECT, 7 | STRING, 8 | TRUE, 9 | FALSE, 10 | NULL, 11 | 12 | // numbers 13 | NUMBER, 14 | BIGINT, 15 | 16 | // typed 17 | U8, 18 | I8A, 19 | U8A, 20 | I16A, 21 | U16A, 22 | F16A, 23 | I32A, 24 | F32A, 25 | U32A, 26 | I64A, 27 | F64A, 28 | U64A, 29 | 30 | // JS types 31 | BUFFER, 32 | RECURSIVE, 33 | ERROR, 34 | REGEXP, 35 | SET, 36 | MAP, 37 | DATE, 38 | DATAVIEW, 39 | IMAGEDATA, 40 | } from './constants.js'; 41 | 42 | import { 43 | asASCII, 44 | asValid, 45 | pushLength, 46 | pushValue, 47 | pushValues, 48 | pushView, 49 | } from './encode/utils.js'; 50 | 51 | import Float16Array from './float16array.js'; 52 | 53 | import * as number from './number.js'; 54 | 55 | /** @typedef {Map} Cache */ 56 | /** @typedef {0|1|2} recursion */ 57 | /** @typedef {{r:recursion, a:number[]|Uint8Array, m:Cache?, $:boolean, _:number}} RAM */ 58 | /** @typedef {{a:number[]|Uint8Array, $:false, _:number}} Recursion */ 59 | 60 | /** 61 | * @typedef {object} Options 62 | * @prop {'all' | 'some' | 'none'} recursion With `all` being the default, everything but `null`, `boolean` and empty `string` will be tracked recursively. With `some`, all primitives get ignored. With `none`, no recursion is ever tracked, leading to *maximum callstack* if present in the encoded data. 63 | * @prop {boolean?} resizable If `true` it will use a growing `ArrayBuffer` instead of an array. 64 | * @prop {ArrayBuffer?} buffer If passed, it will be filled with all encoded *uint8* values. 65 | * @prop {number} maxByteLength If passed, no more than those bytes will ever be allocated. The maximum value is `(2 ** 32) - 1` but here its default is `2 ** 26` (8MB of data, usually plenty for normal operations). See https://tc39.es/ecma262/multipage/structured-data.html#sec-resizable-arraybuffer-guidelines to know more. 66 | */ 67 | 68 | const MAX_BYTE_LENGTH = (2 ** 26); 69 | 70 | const { isArray } = Array; 71 | const { isView } = ArrayBuffer; 72 | const { isFinite } = Number; 73 | const { entries } = Object; 74 | 75 | const ImageData = globalThis.ImageData || class {}; 76 | 77 | const toBufferedClone = Symbol.for('buffered-clone'); 78 | 79 | const encoder = new TextEncoder; 80 | 81 | /** 82 | * @param {Cache} map 83 | * @param {any} value 84 | * @param {number} at 85 | */ 86 | const recursive = (map, value, at) => { 87 | const a = []; 88 | map.set(value, a); 89 | pushLength( 90 | /** @type {Recursion} */({ _: 0, a, $: false }), 91 | RECURSIVE, 92 | at 93 | ); 94 | }; 95 | 96 | class Encoder { 97 | /** 98 | * @param {recursion} r 99 | * @param {number[]|Uint8Array} a 100 | * @param {Cache} m 101 | * @param {boolean} resizable 102 | * @param {boolean} typed 103 | */ 104 | constructor(r, a, m, resizable, typed) { 105 | this._ = 0; 106 | this.r = r; 107 | this.a = a; 108 | this.m = m; 109 | 110 | this.$ = resizable; 111 | this.T = typed; 112 | } 113 | 114 | /** 115 | * @param {any[]} value 116 | */ 117 | array(value) { 118 | this.track(0, value); 119 | const { length } = value; 120 | pushLength(this, ARRAY, length); 121 | for (let i = 0; i < length; i++) 122 | this.encode(value[i], true); 123 | } 124 | 125 | /** 126 | * @param {ArrayBufferLike} value 127 | */ 128 | buffer(value) { 129 | this.track(0, value); 130 | const ui8a = new Uint8Array(value); 131 | this.push(BUFFER, ui8a); 132 | } 133 | 134 | /** 135 | * @param {Date} value 136 | */ 137 | date(value) { 138 | this.track(0, value); 139 | asASCII(this, DATE, value.toISOString()); 140 | } 141 | 142 | /** 143 | * @param {Error} error 144 | */ 145 | error(error) { 146 | this.track(0, error); 147 | pushValue(this, ERROR); 148 | const { name, message } = error; 149 | if (!this.known(name)) asASCII(this, STRING, name); 150 | if (!this.known(message)) this.string(message); 151 | } 152 | 153 | /** 154 | * @param {any} value 155 | * @param {boolean} asNull 156 | */ 157 | encode(value, asNull) { 158 | if (value === null) return pushValue(this, NULL); 159 | if (this.known(value)) return; 160 | switch (asValid(value)) { 161 | case 'object': { 162 | switch (true) { 163 | case toBufferedClone in value: this.indirect(value); break; 164 | case value.constructor === Object: this.generic(value); break; 165 | case isArray(value): this.array(value); break; 166 | //@ts-ignore 167 | case isView(value): this.typed(value); break; 168 | case value instanceof Date: this.date(value); break; 169 | case value instanceof ArrayBuffer: this.buffer(value); break; 170 | case value instanceof Map: this.map(value); break; 171 | case value instanceof Set: this.set(value); break; 172 | case value instanceof RegExp: this.regexp(value); break; 173 | /* c8 ignore next */ 174 | case value instanceof ImageData: this.imageData(/** @type {ImageData} */(value)); break; 175 | case value instanceof Error: this.error(value); break; 176 | // TODO: objects like new Boolean(false) or others 177 | // don't exist in other PLs and I still haven't 178 | // found a use case for those ... only new String 179 | // might be an exception but then again, maybe a 180 | // solution such as toBufferedClone is better here? 181 | default: this.generic(value); break; 182 | } 183 | break; 184 | } 185 | case 'string': this.string(value); break; 186 | case 'number': isFinite(value) ? this.number(NUMBER, value) : pushValue(this, NULL); break; 187 | case 'boolean': pushValue(this, value ? TRUE : FALSE); break; 188 | case 'bigint': this.number(BIGINT, value); break; 189 | default: if (asNull) pushValue(this, NULL); break; 190 | } 191 | } 192 | 193 | /** 194 | * @param {object} value 195 | */ 196 | generic(value) { 197 | this.track(0, value); 198 | const values = []; 199 | for (let pairs = entries(value), j = 0, l = pairs.length; j < l; j++) { 200 | const [k, v] = pairs[j]; 201 | if (asValid(v)) values.push(k, v); 202 | } 203 | this.object(OBJECT, values); 204 | } 205 | 206 | /* c8 ignore next 11 */ 207 | /** 208 | * @param {ImageData} value 209 | */ 210 | imageData(value) { 211 | this.track(0, value); 212 | pushLength(this, IMAGEDATA, 4); 213 | this.encode(value.data, false); 214 | this.number(NUMBER, value.width); 215 | this.number(NUMBER, value.height); 216 | this.object(OBJECT, ['colorSpace', value.colorSpace]); 217 | } 218 | 219 | /** 220 | * @param {object} wrap 221 | */ 222 | indirect(wrap) { 223 | const { _, r, a, m } = this; 224 | const recursion = r > 0; 225 | // store `value` at current position in case 226 | // the returned value also point at itself 227 | if (recursion) recursive(m, wrap, _); 228 | const wrapped = wrap[toBufferedClone](); 229 | // if the method returned itself, make it null 230 | // because there is literally nothing to encode 231 | if (wrapped === wrap) { 232 | if (recursion) { 233 | pushValue(this, NULL); 234 | m.set(wrap, [0]); 235 | } 236 | } 237 | else { 238 | this.encode(wrapped, true); 239 | // if the returned value was not recursive 240 | // avoid multiple invocations of the method 241 | // by storing whatever result it produced 242 | if (recursion && !m.has(wrapped)) 243 | m.set(wrap, /** @type {number[]} */(a.slice(_))); 244 | } 245 | } 246 | 247 | /** 248 | * @param {any} value 249 | * @returns 250 | */ 251 | known(value) { 252 | if (this.r > 0) { 253 | const recursive = this.m.get(value); 254 | if (recursive) { 255 | pushValues(this, recursive); 256 | return true; 257 | } 258 | } 259 | return false; 260 | } 261 | 262 | /** 263 | * @param {Map} value 264 | */ 265 | map(value) { 266 | this.track(0, value); 267 | const values = []; 268 | let i = 0; 269 | for (const [k, v] of value) { 270 | if (asValid(v) && asValid(k)) { 271 | values[i++] = k; 272 | values[i++] = v; 273 | } 274 | } 275 | this.object(MAP, values); 276 | } 277 | 278 | /** 279 | * @param {NUMBER|BIGINT} type 280 | * @param {number|bigint} value 281 | */ 282 | number(type, value) { 283 | this.track(1, value); 284 | const [t, v] = number.encode(type, value); 285 | pushValue(this, t); 286 | if (this.T) pushView(this, v); 287 | else pushValues(this, v); 288 | } 289 | 290 | /** 291 | * @param {number} type 292 | * @param {any[]} values 293 | */ 294 | object(type, values) { 295 | const { length } = values; 296 | pushLength(this, type, length); 297 | for (let i = 0; i < length; i++) 298 | this.encode(values[i], false); 299 | } 300 | 301 | /** 302 | * @param {number} type 303 | * @param {number[]|Uint8Array} view 304 | */ 305 | push(type, view) { 306 | pushLength(this, type, view.length); 307 | if (this.T) pushView(this, view); 308 | else pushValues(this, view); 309 | } 310 | 311 | /** 312 | * @param {RegExp} re 313 | */ 314 | regexp(re) { 315 | this.track(0, re); 316 | pushValue(this, REGEXP); 317 | const { source, flags } = re; 318 | if (!this.known(source)) this.string(source); 319 | if (!this.known(flags)) asASCII(this, STRING, flags); 320 | } 321 | 322 | /** 323 | * @param {Set} value 324 | */ 325 | set(value) { 326 | this.track(0, value); 327 | const values = []; 328 | let i = 0; 329 | for (const v of value) { 330 | if (asValid(v)) values[i++] = v; 331 | } 332 | this.object(SET, values); 333 | } 334 | 335 | /** 336 | * @param {string} value 337 | */ 338 | string(value) { 339 | const vLength = value.length; 340 | if (vLength) { 341 | const { _, r, m } = this; 342 | if (r < 1) { 343 | const str = m.get(value); 344 | if (str) { 345 | if (this.T) pushView(this, str); 346 | else pushValues(this, str); 347 | return; 348 | } 349 | } 350 | this.push(STRING, encoder.encode(value)); 351 | if (r > 1) recursive(m, value, _); 352 | else m.set(value, /** @type {number[]} */(this.a.slice(_))); 353 | } 354 | else pushValues(this, [STRING, U8, 0]); 355 | } 356 | 357 | /** 358 | * @param {recursion} level 359 | * @param {any} value 360 | */ 361 | track(level, value) { 362 | if (this.r > level) 363 | recursive(this.m, value, this._); 364 | } 365 | 366 | /** 367 | * @param {import("./number.js").TypedArray|DataView} view 368 | */ 369 | typed(view) { 370 | this.track(0, view); 371 | let type = NULL; 372 | if (view instanceof Int8Array) type = I8A; 373 | else if (view instanceof Uint8Array) type = U8A; 374 | /* c8 ignore next */ 375 | else if (view instanceof Uint8ClampedArray) type = U8A; 376 | else if (view instanceof Int16Array) type = I16A; 377 | else if (view instanceof Uint16Array) type = U16A; 378 | else if (view instanceof Int32Array) type = I32A; 379 | else if (view instanceof Uint32Array) type = U32A; 380 | else if (view instanceof Float32Array) type = F32A; 381 | else if (view instanceof Float64Array) type = F64A; 382 | else if (view instanceof BigInt64Array) type = I64A; 383 | else if (view instanceof BigUint64Array) type = U64A; 384 | else if (view instanceof DataView) type = DATAVIEW; 385 | /* c8 ignore next */ 386 | else if (/** @type {Float16Array} */(view) instanceof Float16Array) type = F16A; 387 | pushValue(this, type); 388 | if (type !== NULL && !this.known(view.buffer)) this.buffer(view.buffer); 389 | } 390 | } 391 | 392 | /** 393 | * @template T 394 | * @param {T extends undefined ? never : T extends Function ? never : T extends symbol ? never : T} value 395 | * @param {Options?} options 396 | * @returns {Uint8Array} 397 | */ 398 | export default (value, options = null) => { 399 | const maxByteLength = options?.maxByteLength ?? MAX_BYTE_LENGTH; 400 | const recursion = options?.recursion ?? 'all'; 401 | const resizable = !!options?.resizable; 402 | const buffer = options?.buffer; 403 | const typed = resizable || !!buffer; 404 | const r = recursion === 'all' ? 2 : (recursion === 'none' ? 0 : 1); 405 | const a = typed ? 406 | //@ts-ignore 407 | new Uint8Array(buffer || new ArrayBuffer(0, { maxByteLength })) : 408 | [] 409 | ; 410 | const m = new Map; 411 | (new Encoder(r, a, m, resizable, typed)).encode(value, false); 412 | return typed ? /** @type {Uint8Array} */(a) : new Uint8Array(a); 413 | }; 414 | -------------------------------------------------------------------------------- /test/worker/tests.js: -------------------------------------------------------------------------------- 1 | import * as flatted from 'https://esm.run/flatted'; 2 | import * as msgpack from 'https://esm.run/@msgpack/msgpack'; 3 | import * as ungap from 'https://esm.run/@ungap/structured-clone/json'; 4 | import { BSON } from 'https://esm.run/bson'; 5 | import { Encoder, Decoder } from 'https://esm.run/@webreflection/messagepack@0.0.3'; 6 | import JSPack from '../../../node_modules/jspack/src/index.js'; 7 | 8 | const jspack = new JSPack; 9 | const jspackNc = new JSPack({ circular: false }); 10 | 11 | import { data, verify } from '../data.js'; 12 | import { encode, decode } from '../../src/index.js'; 13 | 14 | const carts = await (await fetch('./carts.json')).json(); 15 | 16 | const { encode: wrEncode } = new Encoder({ initialBufferSize: 0x1000000 }); 17 | const { decode: wrDecode } = new Decoder(); 18 | 19 | let makeRecursive = true, cloned = null; 20 | const recursive = () => { 21 | if (makeRecursive) { 22 | makeRecursive = false; 23 | cloned = structuredClone(carts); 24 | cloned.recursive = cloned; 25 | cloned.carts.unshift(cloned); 26 | cloned.carts.push(cloned); 27 | 28 | } 29 | return cloned; 30 | }; 31 | 32 | let makeEncoded = true, encoded = null, jsp_encoded; 33 | const buffer = () => { 34 | if (makeEncoded) { 35 | makeEncoded = false; 36 | encoded = encode(data); 37 | jsp_encoded = jspack.encode(data); 38 | } 39 | return encoded; 40 | }; 41 | 42 | const send = () => [[data]]; 43 | const sendEncoded = () => { 44 | const ui8a = encode(data); 45 | return [[ui8a], [ui8a.buffer]]; 46 | }; 47 | 48 | const checkRecursion = data => { 49 | let ok = 1; 50 | if (data.dataview.buffer !== data.buffer) { 51 | ok = 0; 52 | console.warn('wrong buffer on dataview'); 53 | } 54 | if (data.typed.buffer !== data.buffer) { 55 | ok = 0; 56 | console.warn('wrong buffer on typed array'); 57 | } 58 | if (data.object.recursive !== data) { 59 | ok = -1; 60 | console.error('recursion not working'); 61 | } 62 | return ok; 63 | }; 64 | 65 | export default { 66 | // ['Roundtrip']: [ 67 | // { 68 | // name: 'Structured Clone', 69 | // url: 'structured/roundtrip.js', 70 | // hot: 1, 71 | // send, 72 | // verify, 73 | // }, 74 | // { 75 | // name: 'Structured Clone: double', 76 | // url: 'structured/double.js', 77 | // hot: 1, 78 | // send, 79 | // verify, 80 | // }, 81 | // { 82 | // name: 'Structured Clone: triple', 83 | // url: 'structured/triple.js', 84 | // hot: 1, 85 | // send, 86 | // verify, 87 | // }, 88 | // { 89 | // name: 'Buffered Clone', 90 | // url: 'buffered/roundtrip.js', 91 | // hot: 5, 92 | // decode: data => decode(data), 93 | // send: sendEncoded, 94 | // verify, 95 | // }, 96 | // { 97 | // name: 'Buffered Clone: double', 98 | // url: 'buffered/double.js', 99 | // hot: 5, 100 | // decode: data => decode(data), 101 | // send: sendEncoded, 102 | // verify, 103 | // }, 104 | // { 105 | // name: 'Buffered Clone: triple', 106 | // url: 'buffered/triple.js', 107 | // hot: 5, 108 | // decode: data => decode(data), 109 | // send: sendEncoded, 110 | // verify, 111 | // } 112 | // ], 113 | ['Simple Serialization']: [ 114 | { 115 | name: 'JSON', 116 | url: 'json/serialization.js', 117 | hot: 1, 118 | decode: data => JSON.parse(data), 119 | send: () => [[JSON.stringify(carts)]], 120 | verify(data) { 121 | if (JSON.stringify(data) !== JSON.stringify(carts)) 122 | throw new Error('invalid data'); 123 | } 124 | }, 125 | // { 126 | // name: 'BSON', 127 | // url: 'bson/serialization.js', 128 | // hot: 5, 129 | // decode: data => BSON.deserialize(data), 130 | // send: () => [[BSON.serialize(carts)]], 131 | // verify(data) { 132 | // if (JSON.stringify(data) !== JSON.stringify(carts)) 133 | // throw new Error('invalid data'); 134 | // } 135 | // }, 136 | { 137 | name: 'Flatted', 138 | url: 'flatted/serialization.js', 139 | hot: 5, 140 | decode: data => flatted.parse(data), 141 | send: () => [[flatted.stringify(carts)]], 142 | verify(data) { 143 | if (JSON.stringify(data) !== JSON.stringify(carts)) 144 | throw new Error('invalid data'); 145 | } 146 | }, 147 | { 148 | name: '@ungap structured-clone/json', 149 | url: 'ungap/serialization.js', 150 | hot: 5, 151 | decode: data => ungap.parse(data), 152 | send: () => [[ungap.stringify(carts)]], 153 | verify(data) { 154 | if (JSON.stringify(data) !== JSON.stringify(carts)) 155 | throw new Error('invalid data'); 156 | } 157 | }, 158 | { 159 | name: 'MessagePack', 160 | url: 'msgpack/serialization.js', 161 | hot: 5, 162 | decode: data => msgpack.decode(data), 163 | send: () => [[msgpack.encode(carts)]], 164 | verify(data) { 165 | if (JSON.stringify(data) !== JSON.stringify(carts)) 166 | throw new Error('invalid data'); 167 | } 168 | }, 169 | // { 170 | // name: '@webreflection MessagePack', 171 | // url: 'messagepack/serialization.js', 172 | // hot: 5, 173 | // decode: data => wrDecode(data), 174 | // send: () => [[wrEncode(carts)]], 175 | // verify(data) { 176 | // if (JSON.stringify(data) !== JSON.stringify(carts)) 177 | // throw new Error('invalid data'); 178 | // } 179 | // }, 180 | { 181 | name: 'Buffered Clone', 182 | url: 'buffered/serialization.js', 183 | hot: 5, 184 | recursion: 'all', 185 | decode(data) { 186 | return decode(data, { recursion: this.recursion }); 187 | }, 188 | send() { 189 | const options = { recursion: this.recursion }; 190 | const ui8a = encode(carts, options); 191 | return [[ui8a, options], [ui8a.buffer]]; 192 | }, 193 | verify(data) { 194 | if (JSON.stringify(data) !== JSON.stringify(carts)) 195 | throw new Error('invalid data'); 196 | } 197 | }, 198 | { 199 | name: 'JSPack NC', 200 | url: 'jspack/serialization-nc.js', 201 | hot: 5, 202 | decode: data => jspackNc.decode(data), 203 | send() { 204 | const ui8a = jspackNc.encode(carts); 205 | return [[ui8a], [ui8a.buffer]]; 206 | }, 207 | verify(data) { 208 | if (JSON.stringify(data) !== JSON.stringify(carts)) 209 | throw new Error('invalid data'); 210 | } 211 | }, 212 | { 213 | name: 'JSPack', 214 | url: 'jspack/serialization.js', 215 | hot: 5, 216 | decode: data => jspack.decode(data), 217 | send() { 218 | const ui8a = jspack.encode(carts); 219 | return [[ui8a], [ui8a.buffer]]; 220 | }, 221 | verify(data) { 222 | if (JSON.stringify(data) !== JSON.stringify(carts)) 223 | throw new Error('invalid data'); 224 | } 225 | }, 226 | ], 227 | ['Recursive Serialization']: [ 228 | { 229 | name: 'Flatted', 230 | url: 'flatted/serialization.js', 231 | hot: 3, 232 | decode: data => flatted.parse(data), 233 | send: () => [[flatted.stringify(recursive())]], 234 | verify(result) { 235 | if (flatted.stringify(result) !== flatted.stringify(recursive())) 236 | throw new Error('invalid data'); 237 | }, 238 | }, 239 | { 240 | name: '@ungap structured-clone/json', 241 | url: 'ungap/serialization.js', 242 | hot: 3, 243 | decode: data => ungap.parse(data), 244 | send: () => [[ungap.stringify(recursive())]], 245 | verify(result) { 246 | if (ungap.stringify(result) !== ungap.stringify(recursive())) 247 | throw new Error('invalid data'); 248 | }, 249 | }, 250 | // { 251 | // name: '@webreflection MessagePack', 252 | // url: 'messagepack/serialization.js', 253 | // hot: 3, 254 | // decode: data => wrDecode(data), 255 | // send: () => [[wrEncode(recursive())]], 256 | // verify(result) { 257 | // if (ungap.stringify(result) !== ungap.stringify(recursive())) 258 | // throw new Error('invalid data'); 259 | // }, 260 | // }, 261 | { 262 | name: 'Buffered Clone', 263 | url: 'buffered/serialization.js', 264 | hot: 3, 265 | decode: data => decode(data), 266 | send() { 267 | const ui8a = encode(recursive()); 268 | return [[ui8a], [ui8a.buffer]]; 269 | }, 270 | verify(result) { 271 | const clone = encode(result); 272 | const source = encode(recursive()); 273 | if (clone.length !== source.length || !clone.every((v, i) => v === source[i])) 274 | throw new Error('invalid data'); 275 | } 276 | }, 277 | { 278 | name: 'JSPack', 279 | url: 'jspack/serialization.js', 280 | hot: 3, 281 | decode: data => jspack.decode(data), 282 | send() { 283 | const ui8a = jspack.encode(recursive()); 284 | return [[ui8a], [ui8a.buffer]]; 285 | }, 286 | verify(result) { 287 | if (flatted.stringify(result) !== flatted.stringify(recursive())) 288 | throw new Error('invalid data'); 289 | }, 290 | }, 291 | ], 292 | 293 | ['Complex Serialization']: [ 294 | // both flatted and BSON are out of the equation 295 | // as either these don't support recursion or these 296 | // simply ignore complex JS types 297 | { 298 | name: '@ungap structured-clone/json', 299 | url: 'ungap/serialization.js', 300 | hot: 3, 301 | ok: 1, 302 | warn: true, 303 | decode: data => ungap.parse(data), 304 | send: () => [[ungap.stringify(data)]], 305 | verify(result) { 306 | if (this.warn) { 307 | this.warn = false; 308 | this.ok = checkRecursion(result); 309 | } 310 | if (ungap.stringify(result) !== ungap.stringify(data)) 311 | throw new Error('invalid data'); 312 | return this.ok; 313 | }, 314 | }, 315 | { 316 | name: 'Buffered Clone', 317 | url: 'buffered/serialization.js', 318 | hot: 3, 319 | ok: 1, 320 | warn: true, 321 | decode: data => decode(data), 322 | send() { 323 | const ui8a = encode(data); 324 | return [[ui8a], [ui8a.buffer]]; 325 | }, 326 | verify(result) { 327 | if (this.warn) { 328 | this.warn = false; 329 | this.ok = checkRecursion(result); 330 | } 331 | const clone = encode(result); 332 | const source = encode(data); 333 | if (clone.length !== source.length || !clone.every((v, i) => v === source[i])) 334 | throw new Error('invalid data'); 335 | return this.ok; 336 | } 337 | }, 338 | { 339 | name: 'JSPack', 340 | url: 'jspack/serialization.js', 341 | hot: 3, 342 | ok: 1, 343 | warn: true, 344 | decode: data => jspack.decode(data), 345 | send() { 346 | const ui8a = jspack.encode(data); 347 | return [[ui8a], [ui8a.buffer]]; 348 | }, 349 | verify(result) { 350 | if (this.warn) { 351 | this.warn = false; 352 | this.ok = checkRecursion(result); 353 | } 354 | const clone = jspack.encode(result); 355 | const source = jspack.encode(data); 356 | if (clone.length !== source.length || !clone.every((v, i) => v === source[i])) 357 | throw new Error('invalid data'); 358 | return this.ok; 359 | } 360 | }, 361 | ], 362 | ['Decode Complex Data']: [ 363 | { 364 | name: '@ungap structured-clone/json', 365 | url: 'ungap/decode.js', 366 | hot: 3, 367 | ok: 1, 368 | warn: true, 369 | decode: data => ungap.parse(data), 370 | send: () => [[ungap.stringify(data)]], 371 | verify(result) { 372 | if (this.warn) { 373 | this.warn = false; 374 | this.ok = checkRecursion(result); 375 | } 376 | if (ungap.stringify(result) !== ungap.stringify(data)) 377 | throw new Error('invalid data'); 378 | return this.ok; 379 | } 380 | }, 381 | { 382 | name: 'Buffered Clone', 383 | url: 'buffered/decode.js', 384 | hot: 3, 385 | ok: 1, 386 | warn: true, 387 | decode: data => decode(data), 388 | send: () => [[buffer()]], 389 | verify(result) { 390 | if (this.warn) { 391 | this.warn = false; 392 | this.ok = checkRecursion(result); 393 | } 394 | const clone = encode(result); 395 | const source = buffer(); 396 | if (clone.length !== source.length || !clone.every((v, i) => v === source[i])) 397 | throw new Error('invalid data'); 398 | return this.ok; 399 | } 400 | }, 401 | { 402 | name: 'JSPack', 403 | url: 'jspack/decode.js', 404 | hot: 3, 405 | ok: 1, 406 | warn: true, 407 | decode: data => jspack.decode(data), 408 | send: () => [[jsp_encoded]], 409 | verify(result) { 410 | if (this.warn) { 411 | this.warn = false; 412 | this.ok = checkRecursion(result); 413 | } 414 | const clone = jspack.encode(result); 415 | const source = jsp_encoded; 416 | if (clone.length !== source.length || !clone.every((v, i) => v === source[i])) 417 | throw new Error('invalid data'); 418 | return this.ok; 419 | } 420 | }, 421 | ], 422 | }; 423 | -------------------------------------------------------------------------------- /src/encoder.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import defaultOptions from './options.js'; 4 | import views from './views.js'; 5 | 6 | import { 7 | MAX_ARGS, 8 | Array, 9 | ArrayBuffer, 10 | DataView, 11 | Date, 12 | Error, 13 | ImageData, 14 | Map, 15 | RegExp, 16 | Set, 17 | String, 18 | TypeError, 19 | Uint8Array, 20 | } from './globals.js'; 21 | 22 | 23 | const { isArray } = Array; 24 | const { isView } = ArrayBuffer; 25 | const { isInteger, isFinite } = Number; 26 | const { entries } = Object; 27 | 28 | /** @param {any} value */ 29 | const typeError = value => { 30 | /* c8 ignore next */ 31 | throw new TypeError(`Unable to clone ${String(value)}`); 32 | }; 33 | 34 | const notAscii = /[\u0080-\uFFFF]/; 35 | const te = new TextEncoder; 36 | 37 | export const encoder = ({ 38 | littleEndian = defaultOptions.littleEndian, 39 | circular = defaultOptions.circular, 40 | byteOffset = defaultOptions.byteOffset, 41 | byteLength = defaultOptions.byteLength, 42 | useFloat32 = defaultOptions.useFloat32, 43 | useUTF16 = defaultOptions.useUTF16, 44 | mirrored = defaultOptions.mirrored, 45 | buffer = new ArrayBuffer(byteLength), 46 | } = defaultOptions) => { 47 | let 48 | i = byteOffset, 49 | bufferLength = buffer.byteLength, 50 | data = new DataView(buffer), 51 | view = new Uint8Array(buffer), 52 | isArrayBuffer = buffer instanceof ArrayBuffer 53 | ; 54 | 55 | const isMirrored = 0 < mirrored.length; 56 | 57 | /** @param {ArrayBufferLike} [$] */ 58 | const reBuffer = $ => { 59 | if ($) { 60 | buffer = $; 61 | isArrayBuffer = buffer instanceof ArrayBuffer; 62 | } 63 | else { 64 | //@ts-ignore 65 | buffer = buffer.transferToFixedLength(bufferLength); 66 | } 67 | data = new DataView(buffer); 68 | view = new Uint8Array(buffer); 69 | }; 70 | 71 | /** 72 | * @param {any} value 73 | * @param {Map} cache 74 | */ 75 | const addCircular = (value, cache) => { 76 | const index = i; 77 | typeSize(-4, i - byteOffset); 78 | cache.set(value, view.slice(index, i)); 79 | i = index; 80 | }; 81 | 82 | const resize = length => { 83 | if (bufferLength < length) { 84 | bufferLength = length + byteLength; 85 | if (isArrayBuffer) { 86 | if (buffer.resizable) 87 | /* c8 ignore next */ 88 | buffer.resize(bufferLength); 89 | else 90 | reBuffer(); 91 | } 92 | /* c8 ignore next 3 */ 93 | else { 94 | buffer.grow(bufferLength); 95 | } 96 | } 97 | }; 98 | 99 | /** 100 | * @template T 101 | * @param {T} value 102 | * @param {Map} cache 103 | * @param {(value:T, cache:Map) => void} encoder 104 | */ 105 | const builtin = (value, cache, encoder) => { 106 | known(value, cache) || encoder(value, cache); 107 | }; 108 | 109 | /** @param {number} length */ 110 | const size = length => { 111 | if (length < 0x100) asU8(length); 112 | else if (length < 0x10000) asU16(length); 113 | else asU32(length); 114 | }; 115 | 116 | /** 117 | * @param {number} type 118 | * @param {number} length 119 | */ 120 | const typeSize = (type, length) => { 121 | const index = i++; 122 | size(length); 123 | data.setInt8(index, type); 124 | }; 125 | 126 | const asNull = () => { 127 | resize(i + 1); 128 | data.setInt8(i++, -1); 129 | }; 130 | 131 | const asFalse = () => { 132 | resize(i + 1); 133 | data.setInt8(i++, -2); 134 | }; 135 | 136 | const asTrue = () => { 137 | resize(i + 1); 138 | data.setInt8(i++, -3); 139 | }; 140 | 141 | /** 142 | * @param {string} value 143 | * @param {number} [length] 144 | */ 145 | const asAscii = (value, length = value.length) => { 146 | typeSize(-5, length); 147 | resize(i + length); 148 | for (let j = 0; j < length; j++) view[i++] = value.charCodeAt(j); 149 | }; 150 | 151 | /** 152 | * @param {string} value 153 | * @param {Map} cache 154 | */ 155 | const asString = (value, cache) => { 156 | let length = value.length; 157 | if (length) { 158 | const index = i; 159 | if (circular) addCircular(value, cache); 160 | if (MAX_ARGS <= length || notAscii.test(value)) { 161 | if (useUTF16) { 162 | typeSize(-7, length); 163 | resize(i + (length * 2)); 164 | for (let j = 0; j < length; j++) { 165 | data.setUint16(i, value.charCodeAt(j), littleEndian); 166 | i += 2; 167 | } 168 | } 169 | else { 170 | const ui8a = te.encode(value); 171 | length = ui8a.length; 172 | typeSize(-6, length); 173 | resize(i + length); 174 | view.set(ui8a, i); 175 | i += length; 176 | } 177 | } 178 | else asAscii(value, length); 179 | if (!circular) cache.set(value, view.slice(index, i)); 180 | } 181 | else typeSize(-5, 0); 182 | }; 183 | 184 | /** @param {ArrayBufferLike} value */ 185 | const asBuffer = value => { 186 | const length = value.byteLength; 187 | typeSize(-10, length); 188 | resize(i + length); 189 | view.set(new Uint8Array(value), i); 190 | i += length; 191 | }; 192 | 193 | /** 194 | * @param {any[]} value 195 | * @param {Map} cache 196 | */ 197 | const asArray = (value, cache) => { 198 | const length = value.length; 199 | typeSize(-11, length); 200 | for (let j = 0; j < length; j++) 201 | encode(value[j], cache); 202 | }; 203 | 204 | /** 205 | * @param {Date} value 206 | */ 207 | const asDate = value => { 208 | resize(i + 1); 209 | data.setInt8(i++, -12); 210 | asF64(value.getTime()); 211 | }; 212 | 213 | /** 214 | * @param {object} value 215 | * @param {Map} cache 216 | */ 217 | const asObject = (value, cache) => { 218 | const pairs = entries(value); 219 | const length = pairs.length; 220 | typeSize(-13, length); 221 | for (let j = 0; j < length; j++) { 222 | const pair = pairs[j]; 223 | builtin(pair[0], cache, asString); 224 | encode(pair[1], cache); 225 | } 226 | }; 227 | 228 | /** 229 | * @param {symbol} value 230 | * @param {Map} cache 231 | */ 232 | const asSymbol = (value, cache) => { 233 | /* c8 ignore next */ 234 | let description = value.description || ''; 235 | if (description.startsWith('Symbol.')) 236 | description = description.slice(7); 237 | /* c8 ignore next */ 238 | else if (!Symbol.keyFor(value)) typeError(value); 239 | const length = description.length; 240 | /* c8 ignore next */ 241 | if (!length) typeError(value); 242 | resize(i + 1); 243 | data.setInt8(i++, -14); 244 | asString(description, cache); 245 | }; 246 | 247 | /** 248 | * @param {ArrayBufferView} value 249 | * @param {Map} cache 250 | */ 251 | const asView = (value, cache) => { 252 | for (let j = 0; j < views.length; j++) { 253 | if (value instanceof views[j]) { 254 | const buffer = value.buffer; 255 | resize(i + 2); 256 | data.setInt8(i++, -15); 257 | data.setUint8(i++, j); 258 | if (circular) builtin(buffer, cache, asBuffer); 259 | /* c8 ignore next */ 260 | else asBuffer(buffer); 261 | return; 262 | } 263 | } 264 | /* c8 ignore next */ 265 | typeError(value); 266 | }; 267 | 268 | /** 269 | * @param {Map} value 270 | * @param {Map} cache 271 | */ 272 | const asMap = (value, cache) => { 273 | const pairs = [...value.entries()]; 274 | const length = pairs.length; 275 | typeSize(-16, length); 276 | for (let j = 0; j < length; j++) { 277 | const pair = pairs[j]; 278 | encode(pair[0], cache); 279 | encode(pair[1], cache); 280 | } 281 | }; 282 | 283 | /** 284 | * @param {Set} value 285 | * @param {Map} cache 286 | */ 287 | const asSet = (value, cache) => { 288 | const values = [...value.values()]; 289 | const length = values.length; 290 | typeSize(-17, length); 291 | for (let j = 0; j < length; j++) 292 | encode(values[j], cache); 293 | }; 294 | 295 | /** 296 | * @param {RegExp} value 297 | * @param {Map} cache 298 | */ 299 | const asRegExp = ({ source, flags }, cache) => { 300 | resize(i + 1); 301 | data.setInt8(i++, -18); 302 | asString(source, cache); 303 | asAscii(flags); 304 | }; 305 | 306 | /* c8 ignore next 14 */ 307 | /** 308 | * @param {ImageData} value 309 | * @param {Map} cache 310 | */ 311 | const asImageData = (value, cache) => { 312 | const buffer = value.data.buffer; 313 | resize(i + 1); 314 | data.setInt8(i++, -19); 315 | if (circular) builtin(buffer, cache, asBuffer); 316 | else asBuffer(buffer); 317 | size(value.width); 318 | size(value.height); 319 | asObject({ colorSpace: value.colorSpace }, cache); 320 | }; 321 | 322 | /** 323 | * @param {Error} value 324 | * @param {Map} cache 325 | */ 326 | const asError = ({ name, message }, cache) => { 327 | resize(i + 1); 328 | data.setInt8(i++, -20); 329 | asAscii(name); 330 | asString(message, cache); 331 | }; 332 | 333 | /** 334 | * @param {object} value 335 | * @param {Map} cache 336 | */ 337 | const asJSON = (value, cache) => { 338 | const json = value.toJSON(); 339 | const same = json === value; 340 | if (!same && typeof json === 'object' && json) 341 | encode(json, cache); 342 | else { 343 | const index = i; 344 | if (same) asNull(); 345 | else encode(json, cache); 346 | cache.set(value, view.slice(index, i)); 347 | } 348 | }; 349 | 350 | /** @param {number} value */ 351 | const asI8 = value => { 352 | resize(i + 2); 353 | data.setInt8(i++, -80); 354 | data.setInt8(i++, value); 355 | }; 356 | 357 | /** @param {number} value */ 358 | const asU8 = value => { 359 | resize(i + 2); 360 | data.setInt8(i++, -81); 361 | data.setUint8(i++, value); 362 | }; 363 | 364 | /** @param {number} value */ 365 | const asI16 = value => { 366 | resize(i + 3); 367 | data.setInt8(i, -82); 368 | data.setInt16(i + 1, value, littleEndian); 369 | i += 3; 370 | }; 371 | 372 | /** @param {number} value */ 373 | const asU16 = value => { 374 | resize(i + 3); 375 | data.setInt8(i, -83); 376 | data.setUint16(i + 1, value, littleEndian); 377 | i += 3; 378 | }; 379 | 380 | // /** @param {number} value */ 381 | // const asF16 = value => { 382 | // resize(i + 3); 383 | // data.setInt8(i, -84); 384 | // //@ts-ignore 385 | // data.setFloat16(i + 1, value, littleEndian); 386 | // i += 3; 387 | // }; 388 | 389 | /** @param {number} value */ 390 | const asI32 = value => { 391 | resize(i + 5); 392 | data.setInt8(i, -85); 393 | data.setInt32(i + 1, value, littleEndian); 394 | i += 5; 395 | }; 396 | 397 | /** @param {number} value */ 398 | const asU32 = value => { 399 | resize(i + 5); 400 | data.setInt8(i, -86); 401 | data.setUint32(i + 1, value, littleEndian); 402 | i += 5; 403 | }; 404 | 405 | /* c8 ignore next 6 */ 406 | /** @param {number} value */ 407 | const asF32 = value => { 408 | resize(i + 5); 409 | data.setInt8(i, -87); 410 | data.setFloat32(i + 1, value, littleEndian); 411 | i += 5; 412 | }; 413 | 414 | /** @param {bigint} value */ 415 | const asI64 = value => { 416 | resize(i + 9); 417 | data.setInt8(i, -88); 418 | data.setBigInt64(i + 1, value, littleEndian); 419 | i += 9; 420 | }; 421 | 422 | // -89 423 | /** @param {bigint} value */ 424 | const asU64 = value => { 425 | resize(i + 9); 426 | data.setInt8(i, -89); 427 | data.setBigUint64(i + 1, value, littleEndian); 428 | i += 9; 429 | }; 430 | 431 | // -90 432 | /** @param {number} value */ 433 | const asF64 = value => { 434 | resize(i + 9); 435 | data.setInt8(i, -90); 436 | data.setFloat64(i + 1, value, littleEndian); 437 | i += 9; 438 | }; 439 | 440 | const float = useFloat32 ? asF32 : asF64; 441 | 442 | /** 443 | * @param {any} value 444 | * @param {Map} cache 445 | * @returns 446 | */ 447 | const known = (value, cache) => { 448 | const cached = cache.get(value); 449 | if (cached) { 450 | const length = cached.length; 451 | resize(i + length); 452 | view.set(cached, i); 453 | i += length; 454 | return true; 455 | } 456 | return false; 457 | }; 458 | 459 | /** 460 | * @param {any} value 461 | * @param {Map} cache 462 | */ 463 | const encode = (value, cache) => { 464 | switch (typeof value) { 465 | case 'boolean': { 466 | if (value) asTrue(); 467 | else asFalse(); 468 | break; 469 | } 470 | case 'number': { 471 | if (isInteger(value)) { 472 | if (value < 0) { 473 | if (value >= -0x80) asI8(value); 474 | else if (value >= -0x8000) asI16(value); 475 | else if (value >= -0x80000000) asI32(value); 476 | else asF64(value); 477 | } 478 | /* c8 ignore next 7 */ 479 | else if (value < 0x80) asI8(value); 480 | else if (value < 0x100) asU8(value); 481 | else if (value < 0x8000) asI16(value); 482 | else if (value < 0x10000) asU16(value); 483 | else if (value < 0x80000000) asI32(value); 484 | else if (value < 0x100000000) asU32(value); 485 | else asF64(value); 486 | } 487 | else if (isFinite(value)) float(value); 488 | else asNull(); 489 | break; 490 | } 491 | case 'string': { 492 | builtin(value, cache, asString); 493 | break; 494 | } 495 | case 'bigint': { 496 | if (value < 0n) asI64(value); 497 | else asU64(value); 498 | break; 499 | } 500 | case 'symbol': { 501 | asSymbol(value, cache); 502 | break; 503 | } 504 | case 'object': { 505 | if (value !== null) { 506 | if ((circular || isMirrored) && known(value, cache)) break; 507 | if (circular) addCircular(value, cache); 508 | if ('toJSON' in value) { 509 | if (value instanceof Date) asDate(value); 510 | else asJSON(value, cache); 511 | } 512 | else if (isArray(value)) asArray(value, cache); 513 | else if (isView(value)) asView(value, cache); 514 | else if (value instanceof ArrayBuffer) asBuffer(value); 515 | else if (value instanceof Map) asMap(value, cache); 516 | else if (value instanceof Set) asSet(value, cache); 517 | else if (value instanceof RegExp) asRegExp(value, cache); 518 | /* c8 ignore next */ 519 | else if (value instanceof ImageData) asImageData(/** @type {ImageData} */(value), cache); 520 | else if (value instanceof Error) asError(value, cache); 521 | else asObject(value, cache); 522 | break; 523 | } 524 | } 525 | default: { 526 | asNull(); 527 | break; 528 | } 529 | } 530 | }; 531 | 532 | /** @type {[any,Uint8Array][]} */ 533 | const computed = mirrored.map((value, index) => { 534 | typeSize(-79, index); 535 | const sub = view.slice(0, i); 536 | i = byteOffset; 537 | return [value, sub]; 538 | }); 539 | 540 | /** 541 | * Encode compatible values into a buffer. 542 | * If `into` is `true` (default: `false`) it returns 543 | * the amount of written bytes (as `buffer.byteLength`), 544 | * otherwise it returns a view of the serialized data, 545 | * copying the part of the buffer that was involved. 546 | * @param {any} value 547 | * @param {boolean | ArrayBufferLike} [into=false] 548 | * @returns {Uint8Array | number} 549 | */ 550 | return (value, into = false) => { 551 | if (typeof into !== 'boolean') { 552 | bufferLength = into.byteLength; 553 | reBuffer(into); 554 | } 555 | i = byteOffset; 556 | encode(value, new Map(computed)); 557 | const result = into ? (i - byteOffset) : view.slice(byteOffset, i); 558 | if (isArrayBuffer && i > byteLength && !buffer.resizable) { 559 | bufferLength = byteLength; 560 | reBuffer(); 561 | } 562 | return result; 563 | }; 564 | }; 565 | 566 | export class Encoder { 567 | constructor(options = defaultOptions) { 568 | this.encode = encoder(options); 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /test/worker/carts.json: -------------------------------------------------------------------------------- 1 | {"carts":[{"id":1,"products":[{"id":168,"title":"Charger SXT RWD","price":32999.99,"quantity":3,"total":98999.97,"discountPercentage":13.39,"discountedTotal":85743.87,"thumbnail":"https://cdn.dummyjson.com/products/images/vehicle/Charger%20SXT%20RWD/thumbnail.png"},{"id":78,"title":"Apple MacBook Pro 14 Inch Space Grey","price":1999.99,"quantity":2,"total":3999.98,"discountPercentage":18.52,"discountedTotal":3259.18,"thumbnail":"https://cdn.dummyjson.com/products/images/laptops/Apple%20MacBook%20Pro%2014%20Inch%20Space%20Grey/thumbnail.png"},{"id":183,"title":"Green Oval Earring","price":24.99,"quantity":5,"total":124.94999999999999,"discountPercentage":6.28,"discountedTotal":117.1,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-jewellery/Green%20Oval%20Earring/thumbnail.png"},{"id":100,"title":"Apple Airpods","price":129.99,"quantity":5,"total":649.95,"discountPercentage":12.84,"discountedTotal":566.5,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Apple%20Airpods/thumbnail.png"}],"total":103774.85,"discountedTotal":89686.65,"userId":33,"totalProducts":4,"totalQuantity":15},{"id":2,"products":[{"id":144,"title":"Cricket Helmet","price":44.99,"quantity":4,"total":179.96,"discountPercentage":11.47,"discountedTotal":159.32,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Cricket%20Helmet/thumbnail.png"},{"id":124,"title":"iPhone X","price":899.99,"quantity":4,"total":3599.96,"discountPercentage":8.03,"discountedTotal":3310.88,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/iPhone%20X/thumbnail.png"},{"id":148,"title":"Golf Ball","price":9.99,"quantity":4,"total":39.96,"discountPercentage":11.24,"discountedTotal":35.47,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Golf%20Ball/thumbnail.png"},{"id":122,"title":"iPhone 6","price":299.99,"quantity":3,"total":899.97,"discountPercentage":19.64,"discountedTotal":723.22,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/iPhone%206/thumbnail.png"},{"id":110,"title":"Selfie Lamp with iPhone","price":14.99,"quantity":5,"total":74.95,"discountPercentage":19.87,"discountedTotal":60.06,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Selfie%20Lamp%20with%20iPhone/thumbnail.png"}],"total":4794.8,"discountedTotal":4288.95,"userId":142,"totalProducts":5,"totalQuantity":20},{"id":3,"products":[{"id":98,"title":"Rolex Submariner Watch","price":13999.99,"quantity":1,"total":13999.99,"discountPercentage":16.35,"discountedTotal":11710.99,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-watches/Rolex%20Submariner%20Watch/thumbnail.png"},{"id":125,"title":"Oppo A57","price":249.99,"quantity":5,"total":1249.95,"discountPercentage":16.54,"discountedTotal":1043.21,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Oppo%20A57/thumbnail.png"},{"id":55,"title":"Egg Slicer","price":6.99,"quantity":2,"total":13.98,"discountPercentage":16.04,"discountedTotal":11.74,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Egg%20Slicer/thumbnail.png"},{"id":62,"title":"Ice Cube Tray","price":5.99,"quantity":2,"total":11.98,"discountPercentage":8.25,"discountedTotal":10.99,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Ice%20Cube%20Tray/thumbnail.png"},{"id":132,"title":"Samsung Galaxy S8","price":499.99,"quantity":3,"total":1499.97,"discountPercentage":8.84,"discountedTotal":1367.37,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Samsung%20Galaxy%20S8/thumbnail.png"}],"total":16775.87,"discountedTotal":14144.3,"userId":108,"totalProducts":5,"totalQuantity":13},{"id":4,"products":[{"id":187,"title":"Golden Shoes Woman","price":49.99,"quantity":5,"total":249.95000000000002,"discountPercentage":1.64,"discountedTotal":245.85,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-shoes/Golden%20Shoes%20Woman/thumbnail.png"},{"id":40,"title":"Strawberry","price":3.99,"quantity":5,"total":19.950000000000003,"discountPercentage":4.6,"discountedTotal":19.03,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Strawberry/thumbnail.png"},{"id":156,"title":"Green and Black Glasses","price":34.99,"quantity":5,"total":174.95000000000002,"discountPercentage":4.34,"discountedTotal":167.36,"thumbnail":"https://cdn.dummyjson.com/products/images/sunglasses/Green%20and%20Black%20Glasses/thumbnail.png"},{"id":62,"title":"Ice Cube Tray","price":5.99,"quantity":2,"total":11.98,"discountPercentage":8.25,"discountedTotal":10.99,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Ice%20Cube%20Tray/thumbnail.png"}],"total":456.83,"discountedTotal":443.23,"userId":87,"totalProducts":4,"totalQuantity":17},{"id":5,"products":[{"id":108,"title":"iPhone 12 Silicone Case with MagSafe Plum","price":29.99,"quantity":2,"total":59.98,"discountPercentage":14.68,"discountedTotal":51.17,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/iPhone%2012%20Silicone%20Case%20with%20MagSafe%20Plum/thumbnail.png"},{"id":138,"title":"Baseball Ball","price":8.99,"quantity":5,"total":44.95,"discountPercentage":18.49,"discountedTotal":36.64,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Baseball%20Ball/thumbnail.png"},{"id":157,"title":"Party Glasses","price":19.99,"quantity":2,"total":39.98,"discountPercentage":19.17,"discountedTotal":32.32,"thumbnail":"https://cdn.dummyjson.com/products/images/sunglasses/Party%20Glasses/thumbnail.png"},{"id":8,"title":"Dior J'adore","price":89.99,"quantity":3,"total":269.96999999999997,"discountPercentage":10.79,"discountedTotal":240.84,"thumbnail":"https://cdn.dummyjson.com/products/images/fragrances/Dior%20J'adore/thumbnail.png"},{"id":80,"title":"Huawei Matebook X Pro","price":1399.99,"quantity":5,"total":6999.95,"discountPercentage":9.99,"discountedTotal":6300.65,"thumbnail":"https://cdn.dummyjson.com/products/images/laptops/Huawei%20Matebook%20X%20Pro/thumbnail.png"},{"id":28,"title":"Ice Cream","price":5.49,"quantity":3,"total":16.47,"discountPercentage":10,"discountedTotal":14.82,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Ice%20Cream/thumbnail.png"}],"total":7431.3,"discountedTotal":6676.44,"userId":134,"totalProducts":6,"totalQuantity":20},{"id":6,"products":[{"id":172,"title":"Blue Women's Handbag","price":49.99,"quantity":5,"total":249.95000000000002,"discountPercentage":8.08,"discountedTotal":229.75,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-bags/Blue%20Women's%20Handbag/thumbnail.png"},{"id":112,"title":"TV Studio Camera Pedestal","price":499.99,"quantity":3,"total":1499.97,"discountPercentage":15.69,"discountedTotal":1264.62,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/TV%20Studio%20Camera%20Pedestal/thumbnail.png"},{"id":97,"title":"Rolex Datejust","price":10999.99,"quantity":3,"total":32999.97,"discountPercentage":10.58,"discountedTotal":29508.57,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-watches/Rolex%20Datejust/thumbnail.png"},{"id":128,"title":"Realme C35","price":149.99,"quantity":3,"total":449.97,"discountPercentage":3.97,"discountedTotal":432.11,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Realme%20C35/thumbnail.png"}],"total":35199.86,"discountedTotal":31435.05,"userId":150,"totalProducts":4,"totalQuantity":14},{"id":7,"products":[{"id":167,"title":"300 Touring","price":28999.99,"quantity":5,"total":144999.95,"discountPercentage":11.78,"discountedTotal":127918.96,"thumbnail":"https://cdn.dummyjson.com/products/images/vehicle/300%20Touring/thumbnail.png"},{"id":111,"title":"Selfie Stick Monopod","price":12.99,"quantity":4,"total":51.96,"discountPercentage":10.98,"discountedTotal":46.25,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Selfie%20Stick%20Monopod/thumbnail.png"},{"id":129,"title":"Realme X","price":299.99,"quantity":2,"total":599.98,"discountPercentage":10.13,"discountedTotal":539.2,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Realme%20X/thumbnail.png"}],"total":145651.89,"discountedTotal":128504.41,"userId":86,"totalProducts":3,"totalQuantity":11},{"id":8,"products":[{"id":117,"title":"Sportbike Motorcycle","price":7499.99,"quantity":2,"total":14999.98,"discountPercentage":19.83,"discountedTotal":12025.48,"thumbnail":"https://cdn.dummyjson.com/products/images/motorcycle/Sportbike%20Motorcycle/thumbnail.png"},{"id":18,"title":"Cat Food","price":8.99,"quantity":4,"total":35.96,"discountPercentage":1.15,"discountedTotal":35.55,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Cat%20Food/thumbnail.png"},{"id":105,"title":"Apple MagSafe Battery Pack","price":99.99,"quantity":5,"total":499.95,"discountPercentage":7.14,"discountedTotal":464.25,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Apple%20MagSafe%20Battery%20Pack/thumbnail.png"},{"id":6,"title":"Calvin Klein CK One","price":49.99,"quantity":3,"total":149.97,"discountPercentage":5.67,"discountedTotal":141.47,"thumbnail":"https://cdn.dummyjson.com/products/images/fragrances/Calvin%20Klein%20CK%20One/thumbnail.png"}],"total":15685.86,"discountedTotal":12666.75,"userId":23,"totalProducts":4,"totalQuantity":14},{"id":9,"products":[{"id":178,"title":"Corset Leather With Skirt","price":89.99,"quantity":2,"total":179.98,"discountPercentage":12.59,"discountedTotal":157.32,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-dresses/Corset%20Leather%20With%20Skirt/thumbnail.png"},{"id":191,"title":"Rolex Cellini Moonphase","price":15999.99,"quantity":4,"total":63999.96,"discountPercentage":3.26,"discountedTotal":61913.56,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-watches/Rolex%20Cellini%20Moonphase/thumbnail.png"},{"id":47,"title":"Table Lamp","price":49.99,"quantity":2,"total":99.98,"discountPercentage":13.74,"discountedTotal":86.24,"thumbnail":"https://cdn.dummyjson.com/products/images/home-decoration/Table%20Lamp/thumbnail.png"},{"id":134,"title":"Vivo S1","price":249.99,"quantity":5,"total":1249.95,"discountPercentage":5.64,"discountedTotal":1179.45,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Vivo%20S1/thumbnail.png"}],"total":65529.87,"discountedTotal":63336.57,"userId":194,"totalProducts":4,"totalQuantity":13},{"id":10,"products":[{"id":190,"title":"IWC Ingenieur Automatic Steel","price":4999.99,"quantity":5,"total":24999.949999999997,"discountPercentage":12.34,"discountedTotal":21914.96,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-watches/IWC%20Ingenieur%20Automatic%20Steel/thumbnail.png"},{"id":94,"title":"Longines Master Collection","price":1499.99,"quantity":3,"total":4499.97,"discountPercentage":16.44,"discountedTotal":3760.17,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-watches/Longines%20Master%20Collection/thumbnail.png"}],"total":29499.92,"discountedTotal":25675.13,"userId":160,"totalProducts":2,"totalQuantity":8},{"id":11,"products":[{"id":88,"title":"Nike Air Jordan 1 Red And Black","price":149.99,"quantity":1,"total":149.99,"discountPercentage":17.18,"discountedTotal":124.22,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-shoes/Nike%20Air%20Jordan%201%20Red%20And%20Black/thumbnail.png"},{"id":32,"title":"Milk","price":3.49,"quantity":3,"total":10.47,"discountPercentage":9.36,"discountedTotal":9.49,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Milk/thumbnail.png"},{"id":74,"title":"Spoon","price":4.99,"quantity":3,"total":14.97,"discountPercentage":2.78,"discountedTotal":14.55,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Spoon/thumbnail.png"},{"id":145,"title":"Cricket Wicket","price":29.99,"quantity":3,"total":89.97,"discountPercentage":17.87,"discountedTotal":73.89,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Cricket%20Wicket/thumbnail.png"},{"id":26,"title":"Green Chili Pepper","price":0.99,"quantity":3,"total":2.9699999999999998,"discountPercentage":18.69,"discountedTotal":2.41,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Green%20Chili%20Pepper/thumbnail.png"},{"id":127,"title":"Oppo K1","price":299.99,"quantity":1,"total":299.99,"discountPercentage":15.93,"discountedTotal":252.2,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Oppo%20K1/thumbnail.png"}],"total":568.36,"discountedTotal":476.76,"userId":172,"totalProducts":6,"totalQuantity":14},{"id":12,"products":[{"id":63,"title":"Kitchen Sieve","price":7.99,"quantity":4,"total":31.96,"discountPercentage":18.8,"discountedTotal":25.95,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Kitchen%20Sieve/thumbnail.png"},{"id":181,"title":"Marni Red & Black Suit","price":179.99,"quantity":5,"total":899.95,"discountPercentage":14.21,"discountedTotal":772.07,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-dresses/Marni%20Red%20&%20Black%20Suit/thumbnail.png"}],"total":931.91,"discountedTotal":798.02,"userId":202,"totalProducts":2,"totalQuantity":9},{"id":13,"products":[{"id":85,"title":"Man Plaid Shirt","price":34.99,"quantity":2,"total":69.98,"discountPercentage":3.7,"discountedTotal":67.39,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-shirts/Man%20Plaid%20Shirt/thumbnail.png"},{"id":109,"title":"Monopod","price":19.99,"quantity":3,"total":59.97,"discountPercentage":12.95,"discountedTotal":52.2,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Monopod/thumbnail.png"},{"id":160,"title":"Samsung Galaxy Tab S8 Plus Grey","price":599.99,"quantity":1,"total":599.99,"discountPercentage":4.31,"discountedTotal":574.13,"thumbnail":"https://cdn.dummyjson.com/products/images/tablets/Samsung%20Galaxy%20Tab%20S8%20Plus%20Grey/thumbnail.png"},{"id":163,"title":"Girl Summer Dress","price":19.99,"quantity":3,"total":59.97,"discountPercentage":9.44,"discountedTotal":54.31,"thumbnail":"https://cdn.dummyjson.com/products/images/tops/Girl%20Summer%20Dress/thumbnail.png"},{"id":31,"title":"Lemon","price":0.79,"quantity":4,"total":3.16,"discountPercentage":12.32,"discountedTotal":2.77,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Lemon/thumbnail.png"}],"total":793.07,"discountedTotal":750.8,"userId":41,"totalProducts":5,"totalQuantity":13},{"id":14,"products":[{"id":92,"title":"Sports Sneakers Off White Red","price":109.99,"quantity":3,"total":329.96999999999997,"discountPercentage":17.73,"discountedTotal":271.47,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-shoes/Sports%20Sneakers%20Off%20White%20Red/thumbnail.png"},{"id":54,"title":"Citrus Squeezer Yellow","price":8.99,"quantity":5,"total":44.95,"discountPercentage":6.3,"discountedTotal":42.12,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Citrus%20Squeezer%20Yellow/thumbnail.png"},{"id":76,"title":"Wooden Rolling Pin","price":11.99,"quantity":1,"total":11.99,"discountPercentage":8.45,"discountedTotal":10.98,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Wooden%20Rolling%20Pin/thumbnail.png"},{"id":44,"title":"Family Tree Photo Frame","price":29.99,"quantity":5,"total":149.95,"discountPercentage":10.68,"discountedTotal":133.94,"thumbnail":"https://cdn.dummyjson.com/products/images/home-decoration/Family%20Tree%20Photo%20Frame/thumbnail.png"},{"id":67,"title":"Mug Tree Stand","price":15.99,"quantity":3,"total":47.97,"discountPercentage":16.65,"discountedTotal":39.98,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Mug%20Tree%20Stand/thumbnail.png"},{"id":16,"title":"Apple","price":1.99,"quantity":1,"total":1.99,"discountPercentage":11.74,"discountedTotal":1.76,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Apple/thumbnail.png"}],"total":586.82,"discountedTotal":500.25,"userId":94,"totalProducts":6,"totalQuantity":18},{"id":15,"products":[{"id":11,"title":"Annibale Colombo Bed","price":1899.99,"quantity":5,"total":9499.95,"discountPercentage":8.09,"discountedTotal":8731.4,"thumbnail":"https://cdn.dummyjson.com/products/images/furniture/Annibale%20Colombo%20Bed/thumbnail.png"},{"id":133,"title":"Samsung Galaxy S10","price":699.99,"quantity":3,"total":2099.9700000000003,"discountPercentage":1.12,"discountedTotal":2076.45,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Samsung%20Galaxy%20S10/thumbnail.png"},{"id":111,"title":"Selfie Stick Monopod","price":12.99,"quantity":3,"total":38.97,"discountPercentage":10.98,"discountedTotal":34.69,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Selfie%20Stick%20Monopod/thumbnail.png"},{"id":162,"title":"Blue Frock","price":29.99,"quantity":3,"total":89.97,"discountPercentage":3.86,"discountedTotal":86.5,"thumbnail":"https://cdn.dummyjson.com/products/images/tops/Blue%20Frock/thumbnail.png"},{"id":30,"title":"Kiwi","price":2.49,"quantity":5,"total":12.450000000000001,"discountPercentage":4.34,"discountedTotal":11.91,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Kiwi/thumbnail.png"}],"total":11741.31,"discountedTotal":10940.95,"userId":11,"totalProducts":5,"totalQuantity":19},{"id":16,"products":[{"id":19,"title":"Chicken Meat","price":9.99,"quantity":2,"total":19.98,"discountPercentage":13.37,"discountedTotal":17.31,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Chicken%20Meat/thumbnail.png"},{"id":152,"title":"Tennis Racket","price":49.99,"quantity":3,"total":149.97,"discountPercentage":9.13,"discountedTotal":136.28,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Tennis%20Racket/thumbnail.png"},{"id":35,"title":"Potatoes","price":2.29,"quantity":1,"total":2.29,"discountPercentage":1.69,"discountedTotal":2.25,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Potatoes/thumbnail.png"}],"total":172.24,"discountedTotal":155.84,"userId":200,"totalProducts":3,"totalQuantity":6},{"id":17,"products":[{"id":1,"title":"Essence Mascara Lash Princess","price":9.99,"quantity":2,"total":19.98,"discountPercentage":0.63,"discountedTotal":19.85,"thumbnail":"https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png"},{"id":60,"title":"Grater Black","price":10.99,"quantity":3,"total":32.97,"discountPercentage":16.62,"discountedTotal":27.49,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Grater%20Black/thumbnail.png"},{"id":74,"title":"Spoon","price":4.99,"quantity":4,"total":19.96,"discountPercentage":2.78,"discountedTotal":19.41,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Spoon/thumbnail.png"},{"id":44,"title":"Family Tree Photo Frame","price":29.99,"quantity":4,"total":119.96,"discountPercentage":10.68,"discountedTotal":107.15,"thumbnail":"https://cdn.dummyjson.com/products/images/home-decoration/Family%20Tree%20Photo%20Frame/thumbnail.png"}],"total":192.87,"discountedTotal":173.9,"userId":141,"totalProducts":4,"totalQuantity":13},{"id":18,"products":[{"id":127,"title":"Oppo K1","price":299.99,"quantity":4,"total":1199.96,"discountPercentage":15.93,"discountedTotal":1008.81,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Oppo%20K1/thumbnail.png"},{"id":24,"title":"Fish Steak","price":14.99,"quantity":3,"total":44.97,"discountPercentage":7.66,"discountedTotal":41.53,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Fish%20Steak/thumbnail.png"},{"id":20,"title":"Cooking Oil","price":4.99,"quantity":5,"total":24.950000000000003,"discountPercentage":12.62,"discountedTotal":21.8,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Cooking%20Oil/thumbnail.png"},{"id":154,"title":"Black Sun Glasses","price":29.99,"quantity":3,"total":89.97,"discountPercentage":1.11,"discountedTotal":88.97,"thumbnail":"https://cdn.dummyjson.com/products/images/sunglasses/Black%20Sun%20Glasses/thumbnail.png"},{"id":44,"title":"Family Tree Photo Frame","price":29.99,"quantity":2,"total":59.98,"discountPercentage":10.68,"discountedTotal":53.57,"thumbnail":"https://cdn.dummyjson.com/products/images/home-decoration/Family%20Tree%20Photo%20Frame/thumbnail.png"},{"id":5,"title":"Red Nail Polish","price":8.99,"quantity":5,"total":44.95,"discountPercentage":3.76,"discountedTotal":43.26,"thumbnail":"https://cdn.dummyjson.com/products/images/beauty/Red%20Nail%20Polish/thumbnail.png"}],"total":1464.78,"discountedTotal":1257.94,"userId":189,"totalProducts":6,"totalQuantity":22},{"id":19,"products":[{"id":187,"title":"Golden Shoes Woman","price":49.99,"quantity":3,"total":149.97,"discountPercentage":1.64,"discountedTotal":147.51,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-shoes/Golden%20Shoes%20Woman/thumbnail.png"},{"id":153,"title":"Volleyball","price":11.99,"quantity":5,"total":59.95,"discountPercentage":16.05,"discountedTotal":50.33,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Volleyball/thumbnail.png"},{"id":34,"title":"Nescafe Coffee","price":7.99,"quantity":3,"total":23.97,"discountPercentage":8.31,"discountedTotal":21.98,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Nescafe%20Coffee/thumbnail.png"},{"id":130,"title":"Realme XT","price":349.99,"quantity":2,"total":699.98,"discountPercentage":17.86,"discountedTotal":574.96,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Realme%20XT/thumbnail.png"}],"total":933.87,"discountedTotal":794.78,"userId":59,"totalProducts":4,"totalQuantity":13},{"id":20,"products":[{"id":107,"title":"Beats Flex Wireless Earphones","price":49.99,"quantity":1,"total":49.99,"discountPercentage":8.79,"discountedTotal":45.6,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Beats%20Flex%20Wireless%20Earphones/thumbnail.png"},{"id":193,"title":"Watch Gold for Women","price":799.99,"quantity":3,"total":2399.9700000000003,"discountPercentage":19.53,"discountedTotal":1931.26,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-watches/Watch%20Gold%20for%20Women/thumbnail.png"},{"id":100,"title":"Apple Airpods","price":129.99,"quantity":3,"total":389.97,"discountPercentage":12.84,"discountedTotal":339.9,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Apple%20Airpods/thumbnail.png"},{"id":90,"title":"Puma Future Rider Trainers","price":89.99,"quantity":5,"total":449.95,"discountPercentage":14.7,"discountedTotal":383.81,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-shoes/Puma%20Future%20Rider%20Trainers/thumbnail.png"},{"id":118,"title":"Attitude Super Leaves Hand Soap","price":8.99,"quantity":5,"total":44.95,"discountPercentage":7.23,"discountedTotal":41.7,"thumbnail":"https://cdn.dummyjson.com/products/images/skin-care/Attitude%20Super%20Leaves%20Hand%20Soap/thumbnail.png"},{"id":166,"title":"Tartan Dress","price":39.99,"quantity":5,"total":199.95000000000002,"discountPercentage":2.82,"discountedTotal":194.31,"thumbnail":"https://cdn.dummyjson.com/products/images/tops/Tartan%20Dress/thumbnail.png"}],"total":3534.78,"discountedTotal":2936.58,"userId":90,"totalProducts":6,"totalQuantity":22},{"id":21,"products":[{"id":77,"title":"Yellow Peeler","price":5.99,"quantity":2,"total":11.98,"discountPercentage":13.16,"discountedTotal":10.4,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Yellow%20Peeler/thumbnail.png"},{"id":91,"title":"Sports Sneakers Off White & Red","price":119.99,"quantity":2,"total":239.98,"discountPercentage":1.96,"discountedTotal":235.28,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-shoes/Sports%20Sneakers%20Off%20White%20&%20Red/thumbnail.png"}],"total":251.96,"discountedTotal":245.68,"userId":42,"totalProducts":2,"totalQuantity":4},{"id":22,"products":[{"id":73,"title":"Spice Rack","price":19.99,"quantity":5,"total":99.94999999999999,"discountPercentage":8.74,"discountedTotal":91.21,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Spice%20Rack/thumbnail.png"},{"id":2,"title":"Eyeshadow Palette with Mirror","price":19.99,"quantity":2,"total":39.98,"discountPercentage":0.7,"discountedTotal":39.7,"thumbnail":"https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/thumbnail.png"},{"id":69,"title":"Plate","price":3.99,"quantity":2,"total":7.98,"discountPercentage":16,"discountedTotal":6.7,"thumbnail":"https://cdn.dummyjson.com/products/images/kitchen-accessories/Plate/thumbnail.png"},{"id":155,"title":"Classic Sun Glasses","price":24.99,"quantity":3,"total":74.97,"discountPercentage":9.27,"discountedTotal":68.02,"thumbnail":"https://cdn.dummyjson.com/products/images/sunglasses/Classic%20Sun%20Glasses/thumbnail.png"}],"total":222.88,"discountedTotal":205.63,"userId":140,"totalProducts":4,"totalQuantity":12},{"id":23,"products":[{"id":82,"title":"New DELL XPS 13 9300 Laptop","price":1499.99,"quantity":5,"total":7499.95,"discountPercentage":3.9,"discountedTotal":7207.45,"thumbnail":"https://cdn.dummyjson.com/products/images/laptops/New%20DELL%20XPS%2013%209300%20Laptop/thumbnail.png"},{"id":172,"title":"Blue Women's Handbag","price":49.99,"quantity":4,"total":199.96,"discountPercentage":8.08,"discountedTotal":183.8,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-bags/Blue%20Women's%20Handbag/thumbnail.png"},{"id":41,"title":"Tissue Paper Box","price":2.49,"quantity":2,"total":4.98,"discountPercentage":2.74,"discountedTotal":4.84,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Tissue%20Paper%20Box/thumbnail.png"},{"id":37,"title":"Red Onions","price":1.99,"quantity":4,"total":7.96,"discountPercentage":8.95,"discountedTotal":7.25,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Red%20Onions/thumbnail.png"},{"id":138,"title":"Baseball Ball","price":8.99,"quantity":5,"total":44.95,"discountPercentage":18.49,"discountedTotal":36.64,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Baseball%20Ball/thumbnail.png"}],"total":7757.8,"discountedTotal":7439.98,"userId":147,"totalProducts":5,"totalQuantity":20},{"id":24,"products":[{"id":108,"title":"iPhone 12 Silicone Case with MagSafe Plum","price":29.99,"quantity":5,"total":149.95,"discountPercentage":14.68,"discountedTotal":127.94,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/iPhone%2012%20Silicone%20Case%20with%20MagSafe%20Plum/thumbnail.png"},{"id":134,"title":"Vivo S1","price":249.99,"quantity":4,"total":999.96,"discountPercentage":5.64,"discountedTotal":943.56,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Vivo%20S1/thumbnail.png"},{"id":174,"title":"Prada Women Bag","price":599.99,"quantity":1,"total":599.99,"discountPercentage":12.86,"discountedTotal":522.83,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/thumbnail.png"}],"total":1749.9,"discountedTotal":1594.33,"userId":6,"totalProducts":3,"totalQuantity":10},{"id":25,"products":[{"id":4,"title":"Red Lipstick","price":12.99,"quantity":1,"total":12.99,"discountPercentage":14.69,"discountedTotal":11.08,"thumbnail":"https://cdn.dummyjson.com/products/images/beauty/Red%20Lipstick/thumbnail.png"},{"id":126,"title":"Oppo F19 Pro+","price":399.99,"quantity":1,"total":399.99,"discountPercentage":14.38,"discountedTotal":342.47,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Oppo%20F19%20Pro+/thumbnail.png"}],"total":412.98,"discountedTotal":353.55,"userId":118,"totalProducts":2,"totalQuantity":2},{"id":26,"products":[{"id":37,"title":"Red Onions","price":1.99,"quantity":5,"total":9.95,"discountPercentage":8.95,"discountedTotal":9.06,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Red%20Onions/thumbnail.png"},{"id":128,"title":"Realme C35","price":149.99,"quantity":3,"total":449.97,"discountPercentage":3.97,"discountedTotal":432.11,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/Realme%20C35/thumbnail.png"}],"total":459.92,"discountedTotal":441.17,"userId":66,"totalProducts":2,"totalQuantity":8},{"id":27,"products":[{"id":33,"title":"Mulberry","price":4.99,"quantity":1,"total":4.99,"discountPercentage":2.75,"discountedTotal":4.85,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Mulberry/thumbnail.png"},{"id":110,"title":"Selfie Lamp with iPhone","price":14.99,"quantity":2,"total":29.98,"discountPercentage":19.87,"discountedTotal":24.02,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Selfie%20Lamp%20with%20iPhone/thumbnail.png"},{"id":16,"title":"Apple","price":1.99,"quantity":3,"total":5.97,"discountPercentage":11.74,"discountedTotal":5.27,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Apple/thumbnail.png"},{"id":83,"title":"Blue & Black Check Shirt","price":29.99,"quantity":5,"total":149.95,"discountPercentage":8.76,"discountedTotal":136.81,"thumbnail":"https://cdn.dummyjson.com/products/images/mens-shirts/Blue%20&%20Black%20Check%20Shirt/thumbnail.png"}],"total":190.89,"discountedTotal":170.95,"userId":75,"totalProducts":4,"totalQuantity":11},{"id":28,"products":[{"id":1,"title":"Essence Mascara Lash Princess","price":9.99,"quantity":5,"total":49.95,"discountPercentage":0.63,"discountedTotal":49.64,"thumbnail":"https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png"},{"id":141,"title":"Basketball Rim","price":39.99,"quantity":4,"total":159.96,"discountPercentage":14.7,"discountedTotal":136.45,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Basketball%20Rim/thumbnail.png"}],"total":209.91,"discountedTotal":186.09,"userId":147,"totalProducts":2,"totalQuantity":9},{"id":29,"products":[{"id":80,"title":"Huawei Matebook X Pro","price":1399.99,"quantity":2,"total":2799.98,"discountPercentage":9.99,"discountedTotal":2520.26,"thumbnail":"https://cdn.dummyjson.com/products/images/laptops/Huawei%20Matebook%20X%20Pro/thumbnail.png"},{"id":107,"title":"Beats Flex Wireless Earphones","price":49.99,"quantity":4,"total":199.96,"discountPercentage":8.79,"discountedTotal":182.38,"thumbnail":"https://cdn.dummyjson.com/products/images/mobile-accessories/Beats%20Flex%20Wireless%20Earphones/thumbnail.png"},{"id":25,"title":"Green Bell Pepper","price":1.29,"quantity":2,"total":2.58,"discountPercentage":1.2,"discountedTotal":2.55,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Green%20Bell%20Pepper/thumbnail.png"},{"id":121,"title":"iPhone 5s","price":199.99,"quantity":4,"total":799.96,"discountPercentage":8.38,"discountedTotal":732.92,"thumbnail":"https://cdn.dummyjson.com/products/images/smartphones/iPhone%205s/thumbnail.png"},{"id":153,"title":"Volleyball","price":11.99,"quantity":5,"total":59.95,"discountPercentage":16.05,"discountedTotal":50.33,"thumbnail":"https://cdn.dummyjson.com/products/images/sports-accessories/Volleyball/thumbnail.png"}],"total":3862.43,"discountedTotal":3488.44,"userId":170,"totalProducts":5,"totalQuantity":17},{"id":30,"products":[{"id":181,"title":"Marni Red & Black Suit","price":179.99,"quantity":1,"total":179.99,"discountPercentage":14.21,"discountedTotal":154.41,"thumbnail":"https://cdn.dummyjson.com/products/images/womens-dresses/Marni%20Red%20&%20Black%20Suit/thumbnail.png"},{"id":171,"title":"Pacifica Touring","price":31999.99,"quantity":4,"total":127999.96,"discountPercentage":7.4,"discountedTotal":118527.96,"thumbnail":"https://cdn.dummyjson.com/products/images/vehicle/Pacifica%20Touring/thumbnail.png"},{"id":35,"title":"Potatoes","price":2.29,"quantity":4,"total":9.16,"discountPercentage":1.69,"discountedTotal":9.01,"thumbnail":"https://cdn.dummyjson.com/products/images/groceries/Potatoes/thumbnail.png"},{"id":46,"title":"Plant Pot","price":14.99,"quantity":4,"total":59.96,"discountPercentage":17.65,"discountedTotal":49.38,"thumbnail":"https://cdn.dummyjson.com/products/images/home-decoration/Plant%20Pot/thumbnail.png"}],"total":128249.07,"discountedTotal":118740.76,"userId":177,"totalProducts":4,"totalQuantity":13}],"total":50,"skip":0,"limit":30} --------------------------------------------------------------------------------