From 9d45e42dc0298ea8241132142d3100358fe99dc4 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 20 Nov 2021 07:45:41 +0100 Subject: Adding upstream version 1.2.0. Signed-off-by: Daniel Baumann --- .babelrc | 7 ++++ .eslintrc | 50 +++++++++++++++++++++++ .gitignore | 4 ++ .travis.yml | 1 + LICENSE | 20 +++++++++ README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 33 +++++++++++++++ src/decko.d.ts | 30 ++++++++++++++ src/decko.js | 98 ++++++++++++++++++++++++++++++++++++++++++++ tests/bind.js | 37 +++++++++++++++++ tests/debounce.js | 71 ++++++++++++++++++++++++++++++++ tests/index.ts | 17 ++++++++ tests/memoize.js | 72 ++++++++++++++++++++++++++++++++ tsconfig.json | 24 +++++++++++ 14 files changed, 584 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package.json create mode 100644 src/decko.d.ts create mode 100644 src/decko.js create mode 100644 tests/bind.js create mode 100644 tests/debounce.js create mode 100644 tests/index.ts create mode 100644 tests/memoize.js create mode 100644 tsconfig.json diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..55dee01 --- /dev/null +++ b/.babelrc @@ -0,0 +1,7 @@ +{ + "modules": "umd", + "loose": "all", + "compact": true, + "comments": false, + "stage": 0 +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bb86529 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,50 @@ +{ + "parser": "babel-eslint", + "extends": "eslint:recommended", + "env": { + "browser": true + }, + "ecmaFeatures": { + "modules": true, + "jsx": true + }, + "rules": { + "no-unused-vars": [1, { "args": "after-used" }], + "no-cond-assign": 1, + "semi": 2, + "camelcase": 0, + "comma-style": 2, + "comma-dangle": [2, "never"], + "indent": [2, "tab", {"SwitchCase": 1}], + "no-mixed-spaces-and-tabs": [2, "smart-tabs"], + "no-trailing-spaces": [2, { "skipBlankLines": true }], + "max-nested-callbacks": [2, 3], + "no-eval": 2, + "no-implied-eval": 2, + "no-new-func": 2, + "guard-for-in": 2, + "eqeqeq": 2, + "no-else-return": 2, + "no-redeclare": 2, + "no-dupe-keys": 2, + "radix": 2, + "strict": [2, "never"], + "no-shadow": 0, + "callback-return": [1, ["callback", "cb", "next", "done"]], + "no-delete-var": 2, + "no-undef-init": 2, + "no-shadow-restricted-names": 2, + "handle-callback-err": 0, + "no-lonely-if": 2, + "space-return-throw-case": 2, + "constructor-super": 2, + "no-this-before-super": 2, + "no-dupe-class-members": 2, + "no-const-assign": 2, + "prefer-spread": 2, + "no-useless-concat": 2, + "no-var": 2, + "object-shorthand": 2, + "prefer-arrow-callback": 2 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e1036a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +npm-debug.log +node_modules +dist +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..587bd3e --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: node_js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a38549a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 Jason Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..20f132b --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# decko [![NPM Version](https://img.shields.io/npm/v/decko.svg?style=flat)](https://npmjs.com/package/decko) [![Build Status](https://travis-ci.org/developit/decko.svg?branch=master)](https://travis-ci.org/developit/decko) + +A concise implementation of the three most useful [decorators](https://github.com/wycats/javascript-decorators): + +- `@bind`: make the value of `this` constant within a method +- `@debounce`: throttle calls to a method +- `@memoize`: cache return values based on arguments + +Decorators help simplify code by replacing the noise of common patterns with declarative annotations. +Conversely, decorators can also be overused and create obscurity. +Decko establishes 3 standard decorators that are immediately recognizable, so you can avoid creating decorators in your own codebase. + +> 💡 **Tip:** decko is particularly well-suited to [**Preact Classful Components**](https://github.com/developit/preact). +> +> 💫 **Note:** +> - For Babel 6+, be sure to install [babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy). +> - For Typescript, be sure to enable `{"experimentalDecorators": true}` in your tsconfig.json. + + +## Installation + +Available on [npm](https://npmjs.com/package/decko): + +```sh +npm i -S decko +``` + + +## Usage + +Each decorator method is available as a named import. + +```js +import { bind, memoize, debounce } from 'decko'; +``` + + +### `@bind` + +```js +class Example { + @bind + foo() { + // the value of `this` is always the object from which foo() was referenced. + return this; + } +} + +let e = new Example(); +assert.equal(e.foo.call(null), e); +``` + + + +### `@memoize` + +> Cache values returned from the decorated function. +> Uses the first argument as a cache key. +> _Cache keys are always converted to strings._ +> +> ##### Options: +> +> `caseSensitive: false` - _Makes cache keys case-insensitive_ +> +> `cache: {}` - _Presupply cache storage, for seeding or sharing entries_ + +```js +class Example { + @memoize + expensive(key) { + let start = Date.now(); + while (Date.now()-start < 500) key++; + return key; + } +} + +let e = new Example(); + +// this takes 500ms +let one = e.expensive(1); + +// this takes 0ms +let two = e.expensive(1); + +// this takes 500ms +let three = e.expensive(2); +``` + + + +### `@debounce` + +> Throttle calls to the decorated function. To debounce means "call this at most once per N ms". +> All outward function calls get collated into a single inward call, and only the latest (most recent) arguments as passed on to the debounced function. +> +> ##### Options: +> +> `delay: 0` - _The number of milliseconds to buffer calls for._ + +```js +class Example { + @debounce + foo() { + return this; + } +} + +let e = new Example(); + +// this will only call foo() once: +for (let i=1000; i--) e.foo(); +``` + + +--- + +License +------- + +MIT diff --git a/package.json b/package.json new file mode 100644 index 0000000..c5cf1dc --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "decko", + "version": "1.2.0", + "main": "dist/decko.js", + "types": "dist/decko.d.ts", + "description": "A collection of the most useful property decorators.", + "scripts": { + "build": "mkdir -p dist && babel -f src/decko.js -s -o $npm_package_main < src/decko.js && npm run build:ts", + "build:ts": "cp src/decko.d.ts dist/", + "test": "npm run test:ts && eslint {src,tests}/**.js && mocha --compilers js:babel/register tests/**/*.js", + "test:ts": "tsc -p ./", + "style:ts": "tsfmt -r", + "prepublish": "npm run build", + "release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" + }, + "files": [ + "src", + "dist" + ], + "repository": { + "type": "git", + "url": "git://github.com/developit/decko.git" + }, + "devDependencies": { + "babel": "^5.8.21", + "babel-eslint": "^4.1.6", + "chai": "^3.2.0", + "eslint": "^1.10.3", + "mocha": "^2.3.0", + "typescript": "2.1.6", + "typescript-formatter": "4.1.1" + } +} diff --git a/src/decko.d.ts b/src/decko.d.ts new file mode 100644 index 0000000..e65c737 --- /dev/null +++ b/src/decko.d.ts @@ -0,0 +1,30 @@ +/** + * + */ +export function bind( + target: Object, + propertyKey: string | symbol, + descriptor?: TypedPropertyDescriptor +): TypedPropertyDescriptor | void; +export function bind(): MethodDecorator; + +/** + * @param caseSensitive Makes cache keys case-insensitive + * @param cache Presupply cache storage, for seeding or sharing entries + */ + +export function memoize( + target: Object, + propertyKey: string | symbol, + descriptor?: TypedPropertyDescriptor +): TypedPropertyDescriptor | void; +export function memoize(caseSensitive?: boolean, cache?: Object): MethodDecorator; +/** + * @param delay number + */ +export function debounce( + target: Object, + propertyKey: string | symbol, + descriptor?: TypedPropertyDescriptor +): TypedPropertyDescriptor | void; +export function debounce(delay?: number): MethodDecorator; \ No newline at end of file diff --git a/src/decko.js b/src/decko.js new file mode 100644 index 0000000..298e092 --- /dev/null +++ b/src/decko.js @@ -0,0 +1,98 @@ + +const EMPTY = {}; +const HOP = Object.prototype.hasOwnProperty; + +let fns = { + /** let cachedFn = memoize(originalFn); */ + memoize(fn, opt=EMPTY) { + let cache = opt.cache || {}; + return function(...a) { + let k = String(a[0]); + if (opt.caseSensitive===false) k = k.toLowerCase(); + return HOP.call(cache,k) ? cache[k] : (cache[k] = fn.apply(this, a)); + }; + }, + + /** let throttled = debounce(10, console.log); */ + debounce(fn, opts) { + if (typeof opts==='function') { let p = fn; fn = opts; opts = p; } + let delay = opts && opts.delay || opts || 0, + args, context, timer; + return function(...a) { + args = a; + context = this; + if (!timer) timer = setTimeout( () => { + fn.apply(context, args); + args = context = timer = null; + }, delay); + }; + }, + + bind(target, key, { value: fn }) { + return { + configurable: true, + get() { + let value = fn.bind(this); + Object.defineProperty(this, key, { + value, + configurable: true, + writable: true + }); + return value; + } + }; + } +}; + + +let memoize = multiMethod(fns.memoize), + debounce = multiMethod(fns.debounce), + bind = multiMethod((f,c)=>f.bind(c), ()=>fns.bind); + +export { memoize, debounce, bind }; +export default { memoize, debounce, bind }; + + +/** Creates a function that supports the following calling styles: + * d() - returns an unconfigured decorator + * d(opts) - returns a configured decorator + * d(fn, opts) - returns a decorated proxy to `fn` + * d(target, key, desc) - the decorator itself + * + * @Example: + * // simple identity deco: + * let d = multiMethod( fn => fn ); + * + * class Foo { + * @d + * bar() { } + * + * @d() + * baz() { } + * + * @d({ opts }) + * bat() { } + * + * bap = d(() => {}) + * } + */ +function multiMethod(inner, deco) { + deco = deco || inner.decorate || decorator(inner); + let d = deco(); + return (...args) => { + let l = args.length; + return (l<2 ? deco : (l>2 ? d : inner))(...args); + }; +} + +/** Returns function supports the forms: + * deco(target, key, desc) -> decorate a method + * deco(Fn) -> call the decorator proxy on a function + */ +function decorator(fn) { + return opt => ( + typeof opt==='function' ? fn(opt) : (target, key, desc) => { + desc.value = fn(desc.value, opt, target, key, desc); + } + ); +} diff --git a/tests/bind.js b/tests/bind.js new file mode 100644 index 0000000..c76a0f4 --- /dev/null +++ b/tests/bind.js @@ -0,0 +1,37 @@ +import { bind } from '..'; +import { expect } from 'chai'; + +/*global describe,it*/ + +describe('bind()', () => { + it('should bind when used as a simple decorator', next => { + let c = { + @bind + foo() { + return this; + } + }; + + expect(c.foo()).to.equal(c); + + let p = c.foo; + expect(p()).to.equal(c); + + let a = {}; + expect(c.foo.call(a)).to.equal(c); + + next(); + }); + + it('should bind when used as a function', next => { + let ctx = {}, + c = bind(function(){ return this; }, ctx); + + expect(c()).to.equal(ctx); + + let a = {}; + expect(c.call(a)).to.equal(ctx); + + next(); + }); +}); diff --git a/tests/debounce.js b/tests/debounce.js new file mode 100644 index 0000000..1c8bcf0 --- /dev/null +++ b/tests/debounce.js @@ -0,0 +1,71 @@ +import { debounce } from '..'; +import { expect } from 'chai'; + +/*global describe,it*/ + +describe('debounce()', () => { + it('should debounce when used as a simple decorator', next => { + let c = { + calls: 0, + args: null, + + @debounce + foo(...args) { + c.calls++; + c.args = args; + c.context = this; + } + }; + + expect(c).to.have.property('calls', 0); + c.foo(1); + expect(c).to.have.property('calls', 0); + c.foo(2); + c.foo(3); + setTimeout( () => { + expect(c).to.have.property('calls', 1); + expect(c.args).to.deep.equal([3]); + expect(c.context).to.equal(c); + + next(); + }, 20); + }); + + it('should debounce when used as a function', next => { + let c = debounce( (...args) => { + m.calls++; + m.args = args; + }), + m = { calls:0, args:null }; + + expect(m).to.have.property('calls', 0); + c(1); + expect(m).to.have.property('calls', 0); + c(2); + c(3); + setTimeout( () => { + expect(m).to.have.property('calls', 1); + expect(m.args).to.deep.equal([3]); + + next(); + }, 20); + }); + + it('should support passing a delay', next => { + let c = debounce(5, (...args) => { + m.calls.push(args); + }), + m = { calls:[] }; + + c(1); + setTimeout(()=> c(2), 1); + setTimeout(()=> c(3), 10); + setTimeout(()=> c(4), 14); + setTimeout(()=> c(5), 22); + expect(m.calls).to.have.length(0); + setTimeout( () => { + expect(m.calls).to.deep.equal([ [2], [4], [5] ]); + next(); + }, 30); + }); +}); diff --git a/tests/index.ts b/tests/index.ts new file mode 100644 index 0000000..14c4efe --- /dev/null +++ b/tests/index.ts @@ -0,0 +1,17 @@ +import { bind, debounce, memoize } from '..'; +class C { + @bind + foo() { } + + @debounce + moo() { } + + @debounce(1000) + mooWithCustomDelay() { } + + @memoize + mem() { } + + @memoize(true) + memWithConfig() { } +} \ No newline at end of file diff --git a/tests/memoize.js b/tests/memoize.js new file mode 100644 index 0000000..98f3678 --- /dev/null +++ b/tests/memoize.js @@ -0,0 +1,72 @@ +import { memoize } from '..'; +import { expect } from 'chai'; + +/*global describe,it*/ + +describe('memoize()', () => { + it('should memoize when used as a simple decorator', next => { + let c = { + @memoize + foo(key) { + c[key] = (c[key] || 0) + 1; + } + }; + + expect(c).not.to.have.property('a'); + c.foo('a'); + expect(c).to.have.property('a', 1); + c.foo('a'); + c.foo('a'); + expect(c).to.have.property('a', 1); + + next(); + }); + + it('should memoize when used as a function', next => { + let c = memoize( key => { + m[key] = (m[key] || 0) + 1; + }), + m = {}; + + expect(m).not.to.have.property('a'); + c('a'); + expect(m).to.have.property('a', 1); + c('a'); + c('a'); + expect(m).to.have.property('a', 1); + + next(); + }); + + it('should memoize when called without arguments', next => { + let c = memoize( key => { + m[key] = (m[key] || 0) + 1; + }), + m = {}; + + expect(m).not.to.have.property('undefined'); + c(); + expect(m).to.have.property('undefined', 1); + c(); + c(); + expect(m).to.have.property('undefined', 1); + + next(); + }); + + it('should memoize when called with an empty string', next => { + let c = memoize( key => { + m[key] = (m[key] || 0) + 1; + }), + m = {}; + + expect(m).not.to.have.property(''); + c(''); + expect(m).to.have.property('', 1); + c(''); + c(''); + expect(m).to.have.property('', 1); + + next(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f78fc76 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "es2015", + "target": "es2016", + "lib": [ + "dom", + "es2016" + ], + "baseUrl": "./", + "noImplicitAny": true, + "experimentalDecorators": true, + "sourceMap": false, + "moduleResolution": "node", + "strictNullChecks": true, + "declaration": true, + "noEmit": true, + "pretty": true, + "outDir": "ts-output" + }, + "include": [ + "src/decko.d.ts", + "tests/index.ts" + ] +} -- cgit v1.2.3