Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add constructorHandlers option #40

Merged
merged 6 commits into from
Jun 12, 2024

Conversation

alecgibson
Copy link
Contributor

The motivation of this change is to allow passing custom handlers for particular classes. For example, ObjectId.

These can be passed using the new constructorHandlers option:

const clone = rfdc({
  constructorHandlers: [
    [ObjectId, (o) => new ObjectId(o)],
  ],
})

Similarly, RegExp support can be added manually:

const clone = rfdc({
  constructorHandlers: [
    [RegExp, (o) => new RegExp(o)],
  ],
})

Internally, the special handlers for Date, Map, and Set are moved to use this mechanism to keep code tidy.

Limitations

Note that - for performance - this is backed under the hood by a Map with the classes as keys, which gives constant-time lookup (compared to eg iterating over an array of handlers). A limitation that this introduces is that subclasses would not be matched, and would need their own handlers, since we don't look up the prototype chain.

Performance

Benchmarks before:

benchRfdc*100: 206.839ms
benchRfdcProto*100: 206.776ms
benchRfdcCircles*100: 231.711ms
benchRfdcCirclesProto*100: 229.874ms

Benchmarks after:

benchRfdc*100: 221.126ms
benchRfdcProto*100: 239.467ms
benchRfdcCircles*100: 241.456ms
benchRfdcCirclesProto*100: 257.926ms

@mcollina
Copy link
Collaborator

Can you document this in the README?

index.js Outdated
o2[k] = new Map(cloneArray(Array.from(cur), clone))
} else if (cur instanceof Set) {
o2[k] = new Set(cloneArray(Array.from(cur), clone))
} else if (constructorHandlers.has(cur.constructor)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (constructorHandlers.has(cur.constructor)) {
} else if (cur.constructor !== Object && constructorHandlers.has(cur.constructor)) {

I think we add a fast path wherever there is a .has() to bypass the map lookup.

This should allow to avoid the perf regression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have pushed the change. It improves performance, but not back to where it was before:

benchRfdc*100: 212.82ms
benchRfdcProto*100: 229.3ms
benchRfdcCircles*100: 255.588ms
benchRfdcCirclesProto*100: 238.306ms

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I've assigned in the if check to avoid calling both .has() and .get(), and benchmarks are improved to:

benchRfdc*100: 203.999ms
benchRfdcProto*100: 215.779ms
benchRfdcCircles*100: 224.12ms
benchRfdcCirclesProto*100: 243.172ms

Which is vaguely in-line with before.

Copy link
Collaborator

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
Copy link
Collaborator

Can you rebase your PR? I've fixed CI.

The motivation of this change is to allow passing custom handlers for
particular classes. For example, [`ObjectId`][1].

These can be passed using the new `constructorHandlers` option:

```js
const clone = rfdc({
	constructorHandlers: [
	  [ObjectId, (o) => new ObjectId(o)],
	],
})
```

Similarly, `RegExp` support can be added manually:

```js
const clone = rfdc({
	constructorHandlers: [
	  [RegExp, (o) => new RegExp(o)],
	],
})
```

Internally, the special handlers for `Date`, `Map`, and `Set` are
moved to use this mechanism to keep code tidy.

Limitations
-----------

Note that - for performance - this is backed under the hood by a `Map`
with the classes as keys, which gives constant-time lookup (compared to
eg iterating over an array of handlers). A limitation that this
introduces is that subclasses would not be matched, and would need their
own handlers, since we don't look up the prototype chain.

Performance
-----------

Benchmarks before:

```
benchRfdc*100: 206.839ms
benchRfdcProto*100: 206.776ms
benchRfdcCircles*100: 231.711ms
benchRfdcCirclesProto*100: 229.874ms
```

Benchmarks after:

```
benchRfdc*100: 221.126ms
benchRfdcProto*100: 239.467ms
benchRfdcCircles*100: 241.456ms
benchRfdcCirclesProto*100: 257.926ms
```

[1]: davidmarkclements#7
Improves benchmarks to:

```
benchRfdc*100: 212.82ms
benchRfdcProto*100: 229.3ms
benchRfdcCircles*100: 255.588ms
benchRfdcCirclesProto*100: 238.306ms
```
Improves benchmarks to:

```
benchRfdc*100: 203.999ms
benchRfdcProto*100: 215.779ms
benchRfdcCircles*100: 224.12ms
benchRfdcCirclesProto*100: 243.172ms
```
```
standard: Use JavaScript Standard Style (https://standardjs.com)
  /home/runner/work/rfdc/rfdc/test/index.js:124:25: Use a regular expression literal instead of the 'RegExp' constructor. (prefer-regex-literals)
```
Copy link
Collaborator

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice addition!

index.js Outdated
Comment on lines 16 to 20
const constructorHandlers = new Map([
[Date, (o) => new Date(o)],
[Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))],
[Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))]
].concat(opts.constructorHandlers || []))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const constructorHandlers = new Map([
[Date, (o) => new Date(o)],
[Map, (o, fn) => new Map(cloneArray(Array.from(o), fn))],
[Set, (o, fn) => new Set(cloneArray(Array.from(o), fn))]
].concat(opts.constructorHandlers || []))
const constructorHandlers = new Map()
constructorHandlers.add(Date, (o) => new Date(o))
constructorHandlers.add(Map, (o, fn) => new Map(cloneArray(Array.from(o), fn)))
constructorHandlers.add(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn)))
if (opts.constructorHandlers) {
opts.constructorHandlers.forEach((name, handler) => constructorHandlers.add(name, handler)
}

It is faster to work around the additional array allocation. That could also be done inside of the Map and Set handlers itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see much difference in the benchmark, but will make the change anyway

benchRfdc*100: 206.686ms
benchRfdcProto*100: 218.722ms
benchRfdcCircles*100: 228.578ms
benchRfdcCirclesProto*100: 239.244ms

Updated benchmark

```
benchRfdc*100: 206.686ms
benchRfdcProto*100: 218.722ms
benchRfdcCircles*100: 228.578ms
benchRfdcCirclesProto*100: 239.244ms
```
@mcollina mcollina merged commit 8c6bfec into davidmarkclements:master Jun 12, 2024
8 checks passed
@alecgibson alecgibson deleted the constructor-lookup branch June 12, 2024 09:48
alecgibson added a commit to alecgibson/rfdc that referenced this pull request Jun 12, 2024
mcollina pushed a commit that referenced this pull request Jun 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants