Compare commits

...

1 Commits

Author SHA1 Message Date
Julien Fontanet
048877d653 feat(smart-selector): initial commit 2019-03-27 11:25:26 +01:00
7 changed files with 392 additions and 49 deletions

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -0,0 +1,24 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1,76 @@
# ${pkg.name} [![Build Status](https://travis-ci.org/${pkg.shortGitHubPath}.png?branch=master)](https://travis-ci.org/${pkg.shortGitHubPath})
> ${pkg.description}
Differences with [reselect](https://github.com/reactjs/reselect):
- simpler: no custom memoization
- inputs (and their selectors): are stored in objects, not arrays
- lazy:
- inputs are not computed before accessed
- unused inputs do not trigger a call to the transform function
## Install
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
```
> npm install --save ${pkg.name}
```
## Usage
```js
import createSelector from 'smart-selector'
const getVisibleTodos = createSelector(
{
filter: state => state.filter,
todos: state => state.todos,
},
inputs => {
switch (inputs.filter) {
case 'ALL':
return inputs.todos
case 'COMPLETED':
return inputs.todos.filter(todo => todo.completed)
case 'ACTIVE':
return inputs.todos.filter(todo => !todo.completed)
}
}
)
```
## Development
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xo-web/issues)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1,43 @@
{
"private": true,
"name": "smart-selector",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/smart-selector",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"engines": {
"node": ">=8"
},
"devDependencies": {
"@babel/cli": "7.1.5",
"@babel/core": "7.1.5",
"@babel/preset-env": "7.1.5",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.1",
"rimraf": "^2.6.2"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,82 @@
const { create, keys } = Object
const createSelector = (inputSelectors, transform) => {
const previousArgs = [{}] // initialize with non-repeatable args
let cache, previousResult, previousThisArg
let previousInputs = {}
const spyDescriptors = {}
const inputs = keys(inputSelectors)
for (let i = 0, n = inputs.length; i < n; ++i) {
const input = inputs[i]
spyDescriptors[input] = {
enumerable: true,
get: () =>
input in previousInputs
? previousInputs[input]
: (previousInputs[input] =
input in cache
? cache[input]
: inputSelectors[input].apply(previousThisArg, previousArgs)),
}
}
const spy = create(null, spyDescriptors)
function selector () {
// handle arguments
{
const { length } = arguments
let i = 0
if (this === previousThisArg && length === previousArgs.length) {
while (i < length && arguments[i] === previousArgs[i]) {
++i
}
if (i === length) {
return previousResult
}
} else {
previousArgs.length = length
previousThisArg = this
}
while (i < length) {
previousArgs[i] = arguments[i]
++i
}
}
// handle inputs
cache = previousInputs
previousInputs = {}
{
const inputs = keys(cache)
const { length } = inputs
if (length !== 0) {
let i = 0
while (true) {
if (i === length) {
// inputs are unchanged
return previousResult
}
const input = inputs[i++]
const value = inputSelectors[input].apply(this, arguments)
if (value !== cache[input]) {
// update the value
cache[input] = value
// remove non-computed values
while (i < length) {
delete cache[inputs[i++]]
}
break
}
}
}
}
return (previousResult = transform(spy))
}
return selector
}
export { createSelector as default }

View File

@@ -0,0 +1,99 @@
/* eslint-env jest */
import createSelector from './'
const noop = () => {}
describe('createSelector', () => {
it('calls input selectors with this and arguments', () => {
const thisArg = {}
const args = ['arg1', 'arg2']
const foo = jest.fn()
createSelector({ foo }, ({ foo }) => {}).apply(thisArg, args)
expect(foo.mock.instances).toEqual([thisArg])
expect(foo.mock.calls).toEqual([args])
})
it('calls input selectors only when accessed', () => {
const foo = jest.fn()
createSelector({ foo }, inputs => {
expect(foo.mock.calls.length).toBe(0)
noop(inputs.foo)
expect(foo.mock.calls.length).toBe(1)
})()
})
it('does not call the input selectors if this arguments did not change', () => {
const foo = jest.fn()
const selector = createSelector({ foo }, ({ foo }) => {})
selector('arg1')
expect(foo.mock.calls.length).toBe(1)
selector('arg1')
expect(foo.mock.calls.length).toBe(1)
selector('arg1', 'arg2')
expect(foo.mock.calls.length).toBe(2)
selector.call({}, 'arg1', 'arg2')
expect(foo.mock.calls.length).toBe(3)
})
it('does not call the transform if inputs did not change', () => {
const transform = jest.fn(({ foo }) => {})
const selector = createSelector(
{
foo: () => 'foo',
},
transform
)
selector({})
expect(transform.mock.calls.length).toBe(1)
selector({})
expect(transform.mock.calls.length).toBe(1)
})
it('computes only the necessary inputs to determine if transform should be called', () => {
let foo = 'foo 1'
const bar = 'bar 1'
const inputs = {
foo: jest.fn(() => foo),
bar: jest.fn(() => bar),
}
const transform = jest.fn(inputs => {
if (inputs.foo !== 'foo 1') {
return inputs.bar
}
})
const selector = createSelector(inputs, transform)
selector({})
expect(inputs.foo.mock.calls.length).toBe(1)
expect(inputs.bar.mock.calls.length).toBe(0)
selector({})
expect(inputs.foo.mock.calls.length).toBe(2)
expect(inputs.bar.mock.calls.length).toBe(0)
foo = 'foo 2'
selector({})
expect(inputs.foo.mock.calls.length).toBe(3)
expect(inputs.bar.mock.calls.length).toBe(1)
foo = 'foo 1'
selector({})
expect(inputs.foo.mock.calls.length).toBe(4)
expect(inputs.bar.mock.calls.length).toBe(1)
selector({})
expect(inputs.foo.mock.calls.length).toBe(5)
expect(inputs.bar.mock.calls.length).toBe(1)
})
})

114
yarn.lock
View File

@@ -2,10 +2,10 @@
# yarn lockfile v1
"@babel/cli@^7.0.0", "@babel/cli@^7.1.5":
version "7.2.3"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.2.3.tgz#1b262e42a3e959d28ab3d205ba2718e1923cfee6"
integrity sha512-bfna97nmJV6nDJhXNPeEfxyMjWnt6+IjUAaDPiYRTBlm8L41n8nvw6UAqUCbvpFfU246gHPxW7sfWwqtF4FcYA==
"@babel/cli@7.1.5", "@babel/cli@^7.0.0", "@babel/cli@^7.1.5":
version "7.1.5"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.1.5.tgz#4ccf0a8cdabeefdd8ce955384530f050935bc4d7"
integrity sha512-zbO/DtTnaDappBflIU3zYEgATLToRDmW5uN/EGH1GXaes7ydfjqmAoK++xmJIA+8HfDw7UyPZNdM8fhGhfmMhw==
dependencies:
commander "^2.8.1"
convert-source-map "^1.1.0"
@@ -26,7 +26,27 @@
dependencies:
"@babel/highlight" "^7.0.0"
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.5":
"@babel/core@7.1.5", "@babel/core@^7.0.0", "@babel/core@^7.1.5":
version "7.1.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.5.tgz#abb32d7aa247a91756469e788998db6a72b93090"
integrity sha512-vOyH020C56tQvte++i+rX2yokZcRfbv/kKcw+/BCRw/cK6dvsr47aCzm8oC1XHwMSEWbqrZKzZRLzLnq6SFMsg==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.1.5"
"@babel/helpers" "^7.1.5"
"@babel/parser" "^7.1.5"
"@babel/template" "^7.1.2"
"@babel/traverse" "^7.1.5"
"@babel/types" "^7.1.5"
convert-source-map "^1.1.0"
debug "^3.1.0"
json5 "^0.5.0"
lodash "^4.17.10"
resolve "^1.3.2"
semver "^5.4.1"
source-map "^0.5.0"
"@babel/core@^7.1.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.0.tgz#248fd6874b7d755010bfe61f557461d4f446d9e9"
integrity sha512-Dzl7U0/T69DFOTwqz/FJdnOSWS57NpjNfCwMKHABr589Lg8uX1RrlBIJ7L5Dubt/xkLsx0xH5EBFzlBVes1ayA==
@@ -683,53 +703,49 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5":
version "7.4.2"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.2.tgz#2f5ba1de2daefa9dcca653848f96c7ce2e406676"
integrity sha512-OEz6VOZaI9LW08CWVS3d9g/0jZA6YCn1gsKIy/fut7yZCJti5Lm1/Hi+uo/U+ODm7g4I6gULrCP+/+laT8xAsA==
"@babel/preset-env@7.1.5", "@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5":
version "7.1.5"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.1.5.tgz#a28b5482ca8bc2f2d0712234d6c690240b92495d"
integrity sha512-pQ+2o0YyCp98XG0ODOHJd9z4GsSoV5jicSedRwCrU8uiqcJahwQiOq0asSZEb/m/lwyu6X5INvH/DSiwnQKncw==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
"@babel/plugin-proposal-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread" "^7.4.0"
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.4.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
"@babel/plugin-transform-arrow-functions" "^7.2.0"
"@babel/plugin-transform-async-to-generator" "^7.4.0"
"@babel/plugin-transform-block-scoped-functions" "^7.2.0"
"@babel/plugin-transform-block-scoping" "^7.4.0"
"@babel/plugin-transform-classes" "^7.4.0"
"@babel/plugin-transform-computed-properties" "^7.2.0"
"@babel/plugin-transform-destructuring" "^7.4.0"
"@babel/plugin-transform-dotall-regex" "^7.2.0"
"@babel/plugin-transform-duplicate-keys" "^7.2.0"
"@babel/plugin-transform-exponentiation-operator" "^7.2.0"
"@babel/plugin-transform-for-of" "^7.4.0"
"@babel/plugin-transform-function-name" "^7.2.0"
"@babel/plugin-transform-literals" "^7.2.0"
"@babel/plugin-transform-modules-amd" "^7.2.0"
"@babel/plugin-transform-modules-commonjs" "^7.4.0"
"@babel/plugin-transform-modules-systemjs" "^7.4.0"
"@babel/plugin-transform-modules-umd" "^7.2.0"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.4.2"
"@babel/plugin-transform-new-target" "^7.4.0"
"@babel/plugin-transform-object-super" "^7.2.0"
"@babel/plugin-transform-parameters" "^7.4.0"
"@babel/plugin-transform-regenerator" "^7.4.0"
"@babel/plugin-transform-shorthand-properties" "^7.2.0"
"@babel/plugin-transform-spread" "^7.2.0"
"@babel/plugin-transform-sticky-regex" "^7.2.0"
"@babel/plugin-transform-template-literals" "^7.2.0"
"@babel/plugin-transform-typeof-symbol" "^7.2.0"
"@babel/plugin-transform-unicode-regex" "^7.2.0"
"@babel/types" "^7.4.0"
browserslist "^4.4.2"
core-js-compat "^3.0.0"
"@babel/plugin-proposal-async-generator-functions" "^7.1.0"
"@babel/plugin-proposal-json-strings" "^7.0.0"
"@babel/plugin-proposal-object-rest-spread" "^7.0.0"
"@babel/plugin-proposal-optional-catch-binding" "^7.0.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.0.0"
"@babel/plugin-syntax-async-generators" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.0.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.0.0"
"@babel/plugin-transform-arrow-functions" "^7.0.0"
"@babel/plugin-transform-async-to-generator" "^7.1.0"
"@babel/plugin-transform-block-scoped-functions" "^7.0.0"
"@babel/plugin-transform-block-scoping" "^7.1.5"
"@babel/plugin-transform-classes" "^7.1.0"
"@babel/plugin-transform-computed-properties" "^7.0.0"
"@babel/plugin-transform-destructuring" "^7.0.0"
"@babel/plugin-transform-dotall-regex" "^7.0.0"
"@babel/plugin-transform-duplicate-keys" "^7.0.0"
"@babel/plugin-transform-exponentiation-operator" "^7.1.0"
"@babel/plugin-transform-for-of" "^7.0.0"
"@babel/plugin-transform-function-name" "^7.1.0"
"@babel/plugin-transform-literals" "^7.0.0"
"@babel/plugin-transform-modules-amd" "^7.1.0"
"@babel/plugin-transform-modules-commonjs" "^7.1.0"
"@babel/plugin-transform-modules-systemjs" "^7.0.0"
"@babel/plugin-transform-modules-umd" "^7.1.0"
"@babel/plugin-transform-new-target" "^7.0.0"
"@babel/plugin-transform-object-super" "^7.1.0"
"@babel/plugin-transform-parameters" "^7.1.0"
"@babel/plugin-transform-regenerator" "^7.0.0"
"@babel/plugin-transform-shorthand-properties" "^7.0.0"
"@babel/plugin-transform-spread" "^7.0.0"
"@babel/plugin-transform-sticky-regex" "^7.0.0"
"@babel/plugin-transform-template-literals" "^7.0.0"
"@babel/plugin-transform-typeof-symbol" "^7.0.0"
"@babel/plugin-transform-unicode-regex" "^7.0.0"
browserslist "^4.1.0"
invariant "^2.2.2"
js-levenshtein "^1.1.3"
semver "^5.3.0"