├── .gitignore ├── .npmignore ├── README.md ├── bun.lockb ├── package.json ├── src ├── bench.ts ├── index.ts └── test.ts ├── test └── index.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | lib 178 | 179 | .tap 180 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .tap 2 | node_modules 3 | .DS_Store 4 | test 5 | src 6 | .gitignore 7 | bun.lockdb 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version](https://badge.fury.io/js/sdfclone.svg?style=flat)](https://www.npmjs.com/package/sdfclone) 2 | [![Size](https://img.shields.io/bundlephobia/minzip/sdfclone)](https://gitHub.com/Morglod/sdfclone/) 3 | 4 | # sdfclone 5 | 6 | World fastest schema based deep clone for js (as for Jan 2024) 7 | 8 | Suited for immutable objects with defined structure. 9 | 10 | - ⚡️ +-1% slower than optimal manually written code ⚡️ 11 | - 12x faster than structuredClone 12 | - no deps 13 | - detect cycles (optional) 14 | - support all JSON types 15 | - support most of JS types 16 | - support custom functions 17 | - variants not supported yet 😬 18 | - tested with `rfdc` tests 19 | 20 | ## Usage 21 | 22 | ```js 23 | import { createCloner } from "sdfclone"; 24 | 25 | // define schema 26 | const objectSchema = { 27 | x: Number, 28 | y: { 29 | z: String, 30 | c: { 31 | bb: Date, 32 | bb2: Function, 33 | }, 34 | gg: [{ ff: Number, hh: Number }], 35 | }, 36 | }; 37 | 38 | // create cloner 39 | const cloner = createCloner(objectSchema); 40 | 41 | // or with cycle detector; detectCycles=false by default 42 | const cloner = createCloner(objectSchema, { detectCycles: true }); 43 | 44 | // clone! 45 | const newObject = cloner(object); 46 | ``` 47 | 48 | or less verbose variant: 49 | 50 | ```js 51 | import { createCloner, createCloneSchemaFrom } from "sdfclone"; 52 | 53 | let alreadyCreatedObject = { ... }; 54 | 55 | // extract schema from object 56 | const objectSchema = createCloneSchemaFrom(alreadyCreatedObject); 57 | 58 | // create cloner 59 | const cloner = createCloner(objectSchema); 60 | 61 | // than clone! 62 | const newObject = cloner(alreadyCreatedObject); 63 | ``` 64 | 65 | ## Benchmark 66 | 67 | bun 1.0.21 macbook m1 68 | 69 | ``` 70 | naive x 3,789,560 ops/sec ±0.64% (95 runs sampled) 71 | optimal x 7,908,207 ops/sec ±0.75% (94 runs sampled) <---------------------- 72 | JSON.parse x 1,257,746 ops/sec ±0.33% (98 runs sampled) 73 | sdfclone x 7,825,037 ops/sec ±0.70% (94 runs sampled) <---------------------- 74 | sdfclone detect cycles x 6,582,222 ops/sec ±0.62% (96 runs sampled) 75 | sdfclone with create x 759,116 ops/sec ±0.44% (95 runs sampled) 76 | sdfclone with schema get and create x 535,369 ops/sec ±0.35% (97 runs sampled) 77 | structured clone x 666,819 ops/sec ±0.63% (93 runs sampled) 78 | rfdc x 3,091,196 ops/sec ±0.36% (96 runs sampled) 79 | lodash cloneDeep x 695,643 ops/sec ±0.32% (97 runs sampled) 80 | fastestJsonCopy x 4,042,250 ops/sec ±0.44% (97 runs sampled) 81 | ``` 82 | 83 | node 18.18.0 84 | 85 | ``` 86 | naive x 2,562,532 ops/sec ±0.96% (96 runs sampled) 87 | optimal x 6,024,499 ops/sec ±0.89% (93 runs sampled) <---------------------- 88 | JSON.parse x 467,411 ops/sec ±0.44% (98 runs sampled) 89 | sdfclone x 6,173,111 ops/sec ±0.55% (97 runs sampled) <---------------------- 90 | sdfclone detect cycles x 4,261,157 ops/sec ±0.85% (95 runs sampled) 91 | sdfclone with create x 663,451 ops/sec ±0.51% (96 runs sampled) 92 | sdfclone with schema get and create x 434,197 ops/sec ±0.89% (100 runs sampled) 93 | structured clone x 470,649 ops/sec ±0.45% (99 runs sampled) 94 | rfdc x 2,220,032 ops/sec ±0.26% (97 runs sampled) 95 | lodash cloneDeep x 516,606 ops/sec ±0.31% (99 runs sampled) 96 | fastestJsonCopy x 2,266,253 ops/sec ±0.54% (98 runs sampled) 97 | ``` 98 | 99 | ## Schema description 100 | 101 | ```js 102 | // array 103 | [ itemSchema ] 104 | 105 | // object 106 | { field: fieldSchema } 107 | 108 | // JSON values 109 | null | Number | String | Boolean 110 | 111 | // JS values 112 | undefined | Number | String | Boolean | Function | Symbol | BigInt | Date 113 | 114 | // buffers & typed arrays 115 | Buffer | ArrayBuffer | any TypedArrays 116 | 117 | // specific JS 118 | Map | Set // this uses JSON.parse(JSON.stringify) technique by default 119 | // use `new ClonerMap(valueSchema)` or `new ClonerSet(valueSchema)` when possible 120 | 121 | // for arguments or iterators 122 | // use `new ClonerArrayLike(itemSchema)` 123 | 124 | ``` 125 | 126 | Its also possible to pass custom cloner function with: 127 | `new ClonerCustomFn(customCloner: x => x)` 128 | 129 | ## Utils 130 | 131 | Create schema from existing object: 132 | 133 | ```js 134 | const obj = { ... }; 135 | 136 | // create schema 137 | const objSchema = createCloneSchemaFrom(obj); 138 | 139 | // with option that will also walk inside prototypes cloneProto=false by default 140 | const objSchemaWithProto = createCloneSchemaFrom(obj, { cloneProto: true }); 141 | 142 | const cloner = createCloner(objSchema); 143 | 144 | const newObject = cloner(obj); 145 | ``` 146 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morglod/sdfclone/9f9e68791e71d6fecbc3963dbb6392cf9bec4feb/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdfclone", 3 | "version": "1.0.3", 4 | "description": "World fastest deep clone based on schema", 5 | "types": "lib/index.d.ts", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/Morglod/sdfclone.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Morglod/sdfclone/issues" 13 | }, 14 | "homepage": "https://github.com/Morglod/sdfclone#readme", 15 | "keywords": [ 16 | "deep-clone", 17 | "deepclone", 18 | "deep-copy", 19 | "deepcopy", 20 | "fast", 21 | "performance", 22 | "performant", 23 | "fastclone", 24 | "fastcopy", 25 | "fast-clone", 26 | "fast-deep-clone", 27 | "fast-copy", 28 | "fast-deep-copy", 29 | "typescript" 30 | ], 31 | "author": "morglod", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@types/bun": "latest", 35 | "benchmark": "2.1", 36 | "fastest-json-copy": "^1.0.1", 37 | "lodash": "^4.17.21", 38 | "rfdc": "^1.3.0", 39 | "tap": "^18.6.1", 40 | "typescript": "^5.0.0" 41 | }, 42 | "scripts": { 43 | "test": "tap test", 44 | "build": "tsc", 45 | "bench": "bun run ./src/bench.ts" 46 | }, 47 | "dependencies": {} 48 | } -------------------------------------------------------------------------------- /src/bench.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { createCloner, createCloneSchemaFrom } = require("./index"); 4 | 5 | const benchmark = require("benchmark"); 6 | 7 | const rfdcClone = require("rfdc")(); 8 | 9 | const lodashCloneDeep = require("lodash").cloneDeep; 10 | 11 | const fastestJsonCopy = require("fastest-json-copy").copy; 12 | 13 | function createObject() { 14 | return { 15 | x: Math.random(), 16 | y: { 17 | z: Math.random().toString(), 18 | c: { 19 | bb: Math.random(), 20 | }, 21 | gg: [ 22 | { 23 | ff: 22, 24 | hh: Math.random(), 25 | }, 26 | ], 27 | }, 28 | }; 29 | } 30 | 31 | type ObjT = ReturnType; 32 | 33 | function naiveManualClone(from: ObjT): ObjT { 34 | const obj2 = { 35 | ...from, 36 | y: { 37 | ...from.y, 38 | c: { 39 | ...from.y.c, 40 | }, 41 | gg: from.y.gg.map((x: any) => ({ ...x })), 42 | }, 43 | }; 44 | return obj2; 45 | } 46 | 47 | function optimalManualClone(from: ObjT): ObjT { 48 | const obj2 = { 49 | x: from.x, 50 | y: { 51 | z: from.y.z, 52 | c: { 53 | bb: from.y.c.bb, 54 | }, 55 | gg: from.y.gg.map((x) => ({ 56 | ff: x.ff, 57 | hh: x.hh, 58 | })), 59 | }, 60 | }; 61 | return obj2; 62 | } 63 | 64 | let deopt = 0; 65 | 66 | const clonerSchema = { 67 | x: Number, 68 | y: { 69 | z: String, 70 | c: { 71 | bb: Number, 72 | }, 73 | gg: [{ ff: Number, hh: Number }], 74 | }, 75 | }; 76 | 77 | const cloner = createCloner(clonerSchema, { 78 | detectCycles: false, 79 | }); 80 | 81 | const clonerCycles = createCloner(clonerSchema, { 82 | detectCycles: true, 83 | }); 84 | 85 | new benchmark.Suite() 86 | .add("naive", function () { 87 | const o = createObject(); 88 | const r = naiveManualClone(o); 89 | deopt += r.x + r.y.c.bb; 90 | }) 91 | 92 | .add("optimal", function () { 93 | const o = createObject(); 94 | const r = optimalManualClone(o); 95 | deopt += r.x + r.y.c.bb; 96 | }) 97 | 98 | .add("JSON.parse", function () { 99 | const o = createObject(); 100 | const r = JSON.parse(JSON.stringify(o)); 101 | deopt += r.x + r.y.c.bb; 102 | }) 103 | 104 | .add("sdfclone", function () { 105 | const o = createObject(); 106 | const r = cloner(o); 107 | deopt += r.x + r.y.c.bb; 108 | }) 109 | 110 | .add("sdfclone detect cycles", function () { 111 | const o = createObject(); 112 | const r = clonerCycles(o); 113 | deopt += r.x + r.y.c.bb; 114 | }) 115 | 116 | .add("sdfclone with create", function () { 117 | const o = createObject(); 118 | const r = createCloner(clonerSchema)(o); 119 | deopt += r.x + r.y.c.bb; 120 | }) 121 | 122 | .add("sdfclone with schema get and create", function () { 123 | const o = createObject(); 124 | const r = createCloner(createCloneSchemaFrom(o))(o); 125 | deopt += r.x + r.y.c.bb; 126 | }) 127 | 128 | .add("structured clone", function () { 129 | const o = createObject(); 130 | const r = structuredClone(o); 131 | deopt += r.x + r.y.c.bb; 132 | }) 133 | 134 | .add("rfdc", function () { 135 | const o = createObject(); 136 | const r = rfdcClone(o); 137 | deopt += r.x + r.y.c.bb; 138 | }) 139 | 140 | .add("lodash cloneDeep", function () { 141 | const o = createObject(); 142 | const r = lodashCloneDeep(o); 143 | deopt += r.x + r.y.c.bb; 144 | }) 145 | 146 | .add("fastestJsonCopy", function () { 147 | const o = createObject(); 148 | const r = fastestJsonCopy(o); 149 | deopt += r.x + r.y.c.bb; 150 | }) 151 | 152 | .on("cycle", function cycle(e: any) { 153 | console.log(e.target.toString()); 154 | }) 155 | .on("complete", function completed(this: any) { 156 | console.log("Fastest is %s", this.filter("fastest").map("name")); 157 | }) 158 | .run({ async: true }); 159 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export class ClonerMap { 2 | constructor(public readonly schema: any) {} 3 | } 4 | 5 | export class ClonerSet { 6 | constructor(public readonly schema: any) {} 7 | } 8 | 9 | /** usable eg for arguments */ 10 | export class ClonerArrayLike { 11 | constructor(public readonly schema: any) {} 12 | } 13 | 14 | export class ClonerCustomFn { 15 | constructor(public readonly fn: (x: any) => any) {} 16 | } 17 | 18 | type ClonerOpts = { 19 | detectCycles?: boolean; 20 | }; 21 | 22 | function createClonerCodeCtx() { 23 | return { 24 | cycles: new Map string>(), 25 | customObjs: new Map(), 26 | }; 27 | } 28 | 29 | // browser support 30 | const BufferObject = eval(`(function() { 31 | try { 32 | return Buffer; 33 | } catch { 34 | return Symbol("no buffer"); 35 | } 36 | })`)(); 37 | 38 | export function createClonerCode( 39 | schema: any, 40 | inputName: string, 41 | opts: ClonerOpts, 42 | ctx = createClonerCodeCtx() 43 | ): string { 44 | if (ctx.cycles.has(schema)) { 45 | return ctx.cycles.get(schema)!() + `(${inputName})`; 46 | } 47 | 48 | if ( 49 | schema === Number || 50 | schema === String || 51 | schema === Boolean || 52 | schema === Function || 53 | schema === Symbol 54 | ) { 55 | return inputName; 56 | } else if (schema === BigInt) { 57 | return `new BigInt(${inputName})`; 58 | } else if (schema === Date) { 59 | return `new Date(${inputName})`; 60 | } else if (schema === null) { 61 | return `null`; 62 | } else if (schema === undefined) { 63 | return `undefined`; 64 | } else if ( 65 | schema === BufferObject || 66 | schema === Int8Array || 67 | schema === Uint8Array || 68 | schema === Uint8ClampedArray || 69 | schema === Int16Array || 70 | schema === Uint16Array || 71 | schema === Int32Array || 72 | schema === Uint32Array || 73 | schema === Float32Array || 74 | schema === Float64Array || 75 | schema === BigInt64Array || 76 | schema === BigUint64Array || 77 | schema === ArrayBuffer 78 | ) { 79 | return `${inputName}.subarray()`; 80 | } else if ( 81 | // naive map 82 | schema === Map 83 | ) { 84 | return `(function (inputMap) { 85 | const m = new Map(); 86 | inputMap.forEach(function (v, k) { 87 | m.set(k, JSON.parse(JSON.stringify(v))); 88 | }); 89 | return m; 90 | })(${inputName})`; 91 | } else if ( 92 | // naive set 93 | schema === Set 94 | ) { 95 | return `(function (inputSet) { 96 | const m = new Set(); 97 | inputSet.forEach(function (v) { 98 | m.add(JSON.parse(JSON.stringify(v))); 99 | }); 100 | return m; 101 | })(${inputName})`; 102 | } else if (schema instanceof ClonerMap) { 103 | return `(function (inputMap) { 104 | const m = new Map(); 105 | inputMap.forEach(function (v, k) { 106 | m.set(k, ${createClonerCode(schema.schema, "v", opts, ctx)}); 107 | }); 108 | return m; 109 | })(${inputName})`; 110 | } else if (schema instanceof ClonerSet) { 111 | return `(function (inputSet) { 112 | const m = new Set(); 113 | inputSet.forEach(function (v) { 114 | m.add(${createClonerCode(schema.schema, "v", opts, ctx)}); 115 | }); 116 | return m; 117 | })(${inputName})`; 118 | } else if (schema instanceof ClonerArrayLike) { 119 | const code = `Array.prototype.map.call(${inputName}, function(x) { 120 | return ${createClonerCode(schema.schema, "x", opts, ctx)}; 121 | })`; 122 | 123 | if (opts.detectCycles) { 124 | return `cycleObjs.has(${inputName}) ? cycleObjs.get(${inputName}) : ${code}`; 125 | } 126 | 127 | return code; 128 | } else if (Array.isArray(schema)) { 129 | if (schema.length === 0) { 130 | return "[]"; 131 | } 132 | 133 | if (schema.length > 1) { 134 | throw new Error("unsupported array schema with alternative items"); 135 | } 136 | 137 | const code = `${inputName}.map(function(x) { 138 | return ${createClonerCode(schema[0], "x", opts, ctx)}; 139 | })`; 140 | 141 | if (opts.detectCycles) { 142 | return `cycleObjs.has(${inputName}) ? cycleObjs.get(${inputName}) : ${code}`; 143 | } 144 | 145 | return code; 146 | } else if (schema instanceof ClonerCustomFn) { 147 | if (!ctx.customObjs.has(schema.fn)) { 148 | const id = `customObj_${ctx.customObjs.size}`; 149 | ctx.customObjs.set(schema.fn, id); 150 | } 151 | return `${ctx.customObjs.get(schema)}(${inputName})`; 152 | } else if (typeof schema === "object") { 153 | let code = "{"; 154 | 155 | let cycleGetter = ""; 156 | let cycleFound = false; 157 | const cycleResolver = () => { 158 | if (!opts.detectCycles) 159 | throw new Error("opts.detectCycles not set"); 160 | if (cycleFound) return cycleGetter; 161 | const cycleId = Math.floor(Math.random() * 99999); 162 | code = `(function cycle${cycleId}(input) { if (cycleObjs.has(input)) return cycleObjs.get(input); const obj = {}; cycleObjs.set(input, obj); Object.assign(obj, ${code}`; 163 | cycleGetter = `cycle${cycleId}`; 164 | cycleFound = true; 165 | return cycleGetter; 166 | }; 167 | ctx.cycles.set(schema, cycleResolver); 168 | 169 | for (const field in schema) { 170 | code += field + ":"; 171 | 172 | const clonerCode = createClonerCode( 173 | schema[field], 174 | inputName + "." + field, 175 | opts, 176 | ctx 177 | ); 178 | code += `${clonerCode},`; 179 | } 180 | code += "}"; 181 | 182 | if (cycleFound) { 183 | // TODO: may be bug when nesting circular 184 | code = code.replaceAll(inputName, "input"); 185 | code += `); return obj; })(${inputName})`; 186 | } 187 | 188 | if (opts.detectCycles) { 189 | return `cycleObjs.has(${inputName}) ? cycleObjs.get(${inputName}) : ${code}`; 190 | } 191 | 192 | return code; 193 | } 194 | 195 | throw new Error("unsupported schema root"); 196 | } 197 | 198 | export function createCloner( 199 | schema: any, 200 | opts: ClonerOpts = {} 201 | ): (x: T) => T { 202 | const ctx = createClonerCodeCtx(); 203 | const code = createClonerCode(schema, "input", opts, ctx); 204 | const clonerFnCode = `(function (input) { 205 | ${opts.detectCycles ? `const cycleObjs = new Map();` : ""} 206 | return ${code}; 207 | })`; 208 | 209 | let clonerFn; 210 | 211 | // map custom objs inside 212 | if (ctx.customObjs.size !== 0) { 213 | clonerFn = eval(`(function (customObjs) { 214 | ${Array.from(ctx.customObjs.keys()) 215 | .map((key) => `const ${key} = customObjs.get("${key}");`) 216 | .join("\n")} 217 | 218 | return ${clonerFnCode}; 219 | })`)(ctx.customObjs); 220 | } else { 221 | clonerFn = eval(clonerFnCode); 222 | } 223 | 224 | return clonerFn; 225 | } 226 | 227 | type CreateCloneSchemaFromOpts = { 228 | cloneProto?: boolean; 229 | }; 230 | 231 | export function createCloneSchemaFrom( 232 | x: any, 233 | opts: CreateCloneSchemaFromOpts = {}, 234 | ctx = { cycles: new Map() } 235 | ): any { 236 | if (ctx.cycles.has(x)) return ctx.cycles.get(x); 237 | 238 | if (x === undefined || x === null) return x; 239 | 240 | if (typeof x === "number") return Number; 241 | if (typeof x === "string") return String; 242 | if (typeof x === "boolean") return Boolean; 243 | if (typeof x === "function") return Function; 244 | if (typeof x === "symbol") return Symbol; 245 | if (typeof x === "bigint") return BigInt; 246 | 247 | if (x instanceof Date) return Date; 248 | 249 | if (x instanceof Int8Array) return Int8Array; 250 | if (x instanceof Uint8Array) return Uint8Array; 251 | if (x instanceof Uint8ClampedArray) return Uint8ClampedArray; 252 | if (x instanceof Int16Array) return Int16Array; 253 | if (x instanceof Uint16Array) return Uint16Array; 254 | if (x instanceof Int32Array) return Int32Array; 255 | if (x instanceof Uint32Array) return Uint32Array; 256 | if (x instanceof Float32Array) return Float32Array; 257 | if (x instanceof Float64Array) return Float64Array; 258 | if (x instanceof BigInt64Array) return BigInt64Array; 259 | if (x instanceof BigUint64Array) return BigUint64Array; 260 | if (x instanceof BufferObject) return BufferObject; 261 | if (x instanceof ArrayBuffer) return ArrayBuffer; 262 | 263 | // TODO: implement non naive 264 | if (x instanceof Map) return Map; 265 | if (x instanceof Set) return Set; 266 | 267 | if (!Array.isArray(x) && x[Symbol.iterator] !== undefined) { 268 | console.warn("partial support for iterators"); 269 | if (x.length === 0) return []; 270 | if (x.length > 1) console.warn("unsupported array with variants"); 271 | return new ClonerArrayLike(createCloneSchemaFrom(x[0], opts, ctx)); 272 | } 273 | 274 | if (Array.isArray(x)) { 275 | const obj = [] as any; 276 | ctx.cycles.set(x, obj); 277 | if (x.length === 0) return obj; 278 | if (x.length > 1) console.warn("unsupported array with variants"); 279 | obj.push(createCloneSchemaFrom(x[0], opts, ctx)); 280 | return obj; 281 | } 282 | 283 | if (typeof x === "object") { 284 | const obj = {} as any; 285 | ctx.cycles.set(x, obj); 286 | for (const field in x) { 287 | if (!opts.cloneProto && !Object.hasOwn(x, field)) continue; 288 | obj[field] = createCloneSchemaFrom(x[field], opts, ctx); 289 | } 290 | return obj; 291 | } 292 | 293 | throw new Error("unsupported type"); 294 | } 295 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import { deepEquals } from "bun"; 2 | import { createCloner, createCloneSchemaFrom } from "."; 3 | 4 | function assert(a: any, b: any) { 5 | if (!deepEquals(a, b, true)) { 6 | throw new Error("assertion failed"); 7 | } 8 | } 9 | 10 | { 11 | const numberCloner = createCloner(Number); 12 | assert(numberCloner(10), 10); 13 | } 14 | 15 | { 16 | const numberCloner = createCloner(createCloneSchemaFrom(10)); 17 | assert(numberCloner(10), 10); 18 | } 19 | 20 | { 21 | const dateCloner = createCloner( 22 | createCloneSchemaFrom(new Date(10000)) 23 | ); 24 | assert(dateCloner(new Date(10000)), new Date(10000)); 25 | } 26 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap'); 4 | const tap = require('tap'); 5 | 6 | const { createCloner, createClonerCode, createCloneSchemaFrom } = require('../lib/index.js'); 7 | 8 | function clone(x) { 9 | const schema = createCloneSchemaFrom(x); 10 | try { 11 | const cloner = createCloner(schema); 12 | return cloner(x); 13 | } catch (err) { 14 | const code = createClonerCode(schema, "input", {}); 15 | console.error('-----'); 16 | console.error('generate failed for'); 17 | console.error(x); 18 | console.error('code:'); 19 | console.error(code); 20 | console.error('-----'); 21 | console.error(err); 22 | throw err; 23 | } 24 | } 25 | 26 | function cloneCircles(x) { 27 | const schema = createCloneSchemaFrom(x); 28 | try { 29 | const cloner = createCloner(schema, { detectCycles: true }); 30 | return cloner(x); 31 | } catch (err) { 32 | const code = createClonerCode(schema, "input", { detectCycles: true }); 33 | console.error('-----'); 34 | console.error('generate failed for'); 35 | console.error(x); 36 | console.error('code:'); 37 | console.error(code); 38 | console.error('-----'); 39 | console.error(err); 40 | throw err; 41 | } 42 | } 43 | const rnd = (max) => Math.round(Math.random() * max) 44 | 45 | types(clone, 'default') 46 | 47 | test('default – does not copy proto properties', async ({ equal }) => { 48 | equal(clone(Object.create({ a: 1 })).a, undefined, 'value not copied') 49 | }) 50 | 51 | test('circles option - circular object', async ({ same, equal, not }) => { 52 | const o = { nest: { a: 1, b: 2 } } 53 | o.circular = o 54 | same(cloneCircles(o), o, 'same values') 55 | not(cloneCircles(o), o, 'different objects') 56 | not(cloneCircles(o).nest, o.nest, 'different nested objects') 57 | const c = cloneCircles(o) 58 | equal(c.circular, c, 'circular references point to copied parent') 59 | not(c.circular, o, 'circular references do not point to original parent') 60 | }) 61 | test('circles option – deep circular object', async ({ same, equal, not }) => { 62 | const o = { nest: { a: 1, b: 2 } } 63 | o.nest.circular = o 64 | same(cloneCircles(o), o, 'same values') 65 | not(cloneCircles(o), o, 'different objects') 66 | not(cloneCircles(o).nest, o.nest, 'different nested objects') 67 | const c = cloneCircles(o) 68 | equal(c.nest.circular, c, 'circular references point to copied parent') 69 | not( 70 | c.nest.circular, 71 | o, 72 | 'circular references do not point to original parent' 73 | ) 74 | }) 75 | 76 | function types(clone, label) { 77 | test(label + ' – number', async ({ equal }) => { 78 | equal(clone(42), 42, 'same value') 79 | }) 80 | test(label + ' – string', async ({ equal }) => { 81 | equal(clone('str'), 'str', 'same value') 82 | }) 83 | test(label + ' – boolean', async ({ equal }) => { 84 | equal(clone(true), true, 'same value') 85 | }) 86 | test(label + ' – function', async ({ equal }) => { 87 | const fn = () => { } 88 | equal(clone(fn), fn, 'same function') 89 | }) 90 | test(label + ' – async function', async ({ equal }) => { 91 | const fn = async () => { } 92 | equal(clone(fn), fn, 'same function') 93 | }) 94 | test(label + ' – generator function', async ({ equal }) => { 95 | const fn = function* () { } 96 | equal(clone(fn), fn, 'same function') 97 | }) 98 | test(label + ' – date', async ({ equal, not }) => { 99 | const date = new Date() 100 | equal(+clone(date), +date, 'same value') 101 | not(clone(date), date, 'different object') 102 | }) 103 | test(label + ' – null', async ({ equal }) => { 104 | equal(clone(null), null, 'same value') 105 | }) 106 | test(label + ' – shallow object', async ({ same, not }) => { 107 | const o = { a: 1, b: 2 } 108 | same(clone(o), o, 'same values') 109 | not(clone(o), o, 'different object') 110 | }) 111 | test(label + ' – shallow array', async ({ same, not }) => { 112 | const o = [1, 2] 113 | same(clone(o), o, 'same values') 114 | not(clone(o), o, 'different arrays') 115 | }) 116 | test(label + ' – deep object', async ({ same, not }) => { 117 | const o = { nest: { a: 1, b: 2 } } 118 | same(clone(o), o, 'same values') 119 | not(clone(o), o, 'different objects') 120 | not(clone(o).nest, o.nest, 'different nested objects') 121 | }) 122 | // TODO: 123 | // test(label + ' – deep array', async ({ same, not }) => { 124 | // const o = [{ a: 1, b: 2 }, [3]] 125 | // same(clone(o), o, 'same values') 126 | // not(clone(o), o, 'different arrays') 127 | // not(clone(o)[0], o[0], 'different array elements') 128 | // not(clone(o)[1], o[1], 'different array elements') 129 | // }) 130 | test(label + ' – nested number', async ({ equal }) => { 131 | equal(clone({ a: 1 }).a, 1, 'same value') 132 | }) 133 | test(label + ' – nested string', async ({ equal }) => { 134 | equal(clone({ s: 'str' }).s, 'str', 'same value') 135 | }) 136 | test(label + ' – nested boolean', async ({ equal }) => { 137 | equal(clone({ b: true }).b, true, 'same value') 138 | }) 139 | test(label + ' – nested function', async ({ equal }) => { 140 | const fn = () => { } 141 | equal(clone({ fn }).fn, fn, 'same function') 142 | }) 143 | test(label + ' – nested async function', async ({ equal }) => { 144 | const fn = async () => { } 145 | equal(clone({ fn }).fn, fn, 'same function') 146 | }) 147 | test(label + ' – nested generator function', async ({ equal }) => { 148 | const fn = function* () { } 149 | equal(clone({ fn }).fn, fn, 'same function') 150 | }) 151 | test(label + ' – nested date', async ({ equal, not }) => { 152 | const date = new Date() 153 | equal(+clone({ d: date }).d, +date, 'same value') 154 | not(clone({ d: date }).d, date, 'different object') 155 | }) 156 | test(label + ' – nested date in array', async ({ equal, not }) => { 157 | const date = new Date() 158 | equal(+clone({ d: [date] }).d[0], +date, 'same value') 159 | not(clone({ d: [date] }).d[0], date, 'different object') 160 | equal(+cloneCircles({ d: [date] }).d[0], +date, 'same value') 161 | not(cloneCircles({ d: [date] }).d, date, 'different object') 162 | }) 163 | test(label + ' – nested null', async ({ equal }) => { 164 | equal(clone({ n: null }).n, null, 'same value') 165 | }) 166 | test(label + ' – arguments', async ({ not, same }) => { 167 | function fn(...args) { 168 | same(clone(arguments), args, 'same values') 169 | not(clone(arguments), arguments, 'different object') 170 | } 171 | fn(1, 2, 3) 172 | }) 173 | test(`${label} copies buffers from object correctly`, async ({ ok, equal, not }) => { 174 | const input = Date.now().toString(36) 175 | const inputBuffer = Buffer.from(input) 176 | const clonedBuffer = clone({ a: inputBuffer }).a 177 | tap.ok(Buffer.isBuffer(clonedBuffer), 'cloned value equal buffer') 178 | not(clonedBuffer, inputBuffer, 'cloned buffer equal not same as input buffer') 179 | equal(clonedBuffer.toString(), input, 'cloned buffer content equal correct') 180 | }) 181 | test(`${label} copies buffers from arrays correctly`, async ({ ok, equal, not }) => { 182 | const input = Date.now().toString(36) 183 | const inputBuffer = Buffer.from(input) 184 | const [clonedBuffer] = clone([inputBuffer]) 185 | tap.ok(Buffer.isBuffer(clonedBuffer), 'cloned value equal buffer') 186 | not(clonedBuffer, inputBuffer, 'cloned buffer equal not same as input buffer') 187 | equal(clonedBuffer.toString(), input, 'cloned buffer content equal correct') 188 | }) 189 | test(`${label} copies TypedArrays from object correctly`, async ({ ok, equal, not }) => { 190 | const [input1, input2] = [rnd(10), rnd(10)] 191 | var buffer = new ArrayBuffer(8) 192 | const int32View = new Int32Array(buffer) 193 | int32View[0] = input1 194 | int32View[1] = input2 195 | const cloned = clone({ a: int32View }).a 196 | tap.ok(cloned instanceof Int32Array, 'cloned value equal instance of class') 197 | not(cloned, int32View, 'cloned value equal not same as input value') 198 | equal(cloned[0], input1, 'cloned value content equal correct') 199 | equal(cloned[1], input2, 'cloned value content equal correct') 200 | }) 201 | test(`${label} copies TypedArrays from array correctly`, async ({ ok, equal, not }) => { 202 | const [input1, input2] = [rnd(10), rnd(10)] 203 | var buffer = new ArrayBuffer(16) 204 | const int32View = new Int32Array(buffer) 205 | int32View[0] = input1 206 | int32View[1] = input2 207 | const [cloned] = clone([int32View]) 208 | tap.ok(cloned instanceof Int32Array, 'cloned value equal instance of class') 209 | not(cloned, int32View, 'cloned value equal not same as input value') 210 | equal(cloned[0], input1, 'cloned value content equal correct') 211 | equal(cloned[1], input2, 'cloned value content equal correct') 212 | }) 213 | test(`${label} copies complex TypedArrays`, async ({ ok, same, equal, not }) => { 214 | const [input1, input2, input3] = [rnd(10), rnd(10), rnd(10)] 215 | var buffer = new ArrayBuffer(4) 216 | const view1 = new Int8Array(buffer, 0, 2) 217 | const view2 = new Int8Array(buffer, 2, 2) 218 | const view3 = new Int8Array(buffer) 219 | view1[0] = input1 220 | view2[0] = input2 221 | view3[3] = input3 222 | const cloned = clone({ view1, view2, view3 }) 223 | ok(cloned.view1 instanceof Int8Array, 'cloned value equal instance of class') 224 | ok(cloned.view2 instanceof Int8Array, 'cloned value equal instance of class') 225 | ok(cloned.view3 instanceof Int8Array, 'cloned value equal instance of class') 226 | not(cloned.view1, view1, 'cloned value equal not same as input value') 227 | not(cloned.view2, view2, 'cloned value equal not same as input value') 228 | not(cloned.view3, view3, 'cloned value equal not same as input value') 229 | same(Array.from(cloned.view1), [input1, 0], 'cloned value content equal correct') 230 | same(Array.from(cloned.view2), [input2, input3], 'cloned value content equal correct') 231 | same(Array.from(cloned.view3), [input1, 0, input2, input3], 'cloned value content equal correct') 232 | }) 233 | test(`${label} - maps`, async ({ same, not }) => { 234 | const map = new Map([['a', 1]]) 235 | same(Array.from(clone(map)), [['a', 1]], 'same value') 236 | not(clone(map), map, 'different object') 237 | }) 238 | test(`${label} - sets`, async ({ same, not }) => { 239 | const set = new Set([1]) 240 | same(Array.from(clone(set)), [1]) 241 | not(clone(set), set, 'different object') 242 | }) 243 | test(`${label} - nested maps`, async ({ same, not }) => { 244 | const data = { m: new Map([['a', 1]]) } 245 | same(Array.from(clone(data).m), [['a', 1]], 'same value') 246 | not(clone(data).m, data.m, 'different object') 247 | }) 248 | test(`${label} - nested sets`, async ({ same, not }) => { 249 | const data = { s: new Set([1]) } 250 | same(Array.from(clone(data).s), [1], 'same value') 251 | not(clone(data).s, data.s, 'different object') 252 | }) 253 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2020"], 4 | "target": "ES2015", 5 | "module": "Node16", 6 | "rootDir": "./src", 7 | "outDir": "./lib", 8 | "declaration": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "Node16", 12 | 13 | /* Linting */ 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "forceConsistentCasingInFileNames": true 18 | }, 19 | "include": ["src"], 20 | "exclude": ["test/**"] 21 | } 22 | --------------------------------------------------------------------------------