├── .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 | [](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}
--------------------------------------------------------------------------------