Skip to content

Commit 0773247

Browse files
authored
Merge pull request #1 from github/feat-initial-implementation
Initial Implementation
2 parents e4e80c1 + 3e59dfd commit 0773247

12 files changed

+2962
-0
lines changed

.github/workflows/nodejs.yml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Node CI
2+
3+
on: [push]
4+
jobs:
5+
build:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v2
9+
- name: Use Node.js 14.x
10+
uses: actions/setup-node@v1
11+
with:
12+
node-version: 14.x
13+
- name: npm install, build, and test
14+
run: |
15+
npm install
16+
npm run build --if-present
17+
npm test
18+
env:
19+
CI: true
20+

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @github/web-systems-reviewers

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 GitHub, Inc.
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 is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
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 IN THE
21+
SOFTWARE.

README.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# memoize
2+
3+
This is a package which provides a [`memoize`](https://en.wikipedia.org/wiki/Memoization) function, as well as a TypeScript
4+
decorator which will [memoize](https://en.wikipedia.org/wiki/Memoization) a class method.
5+
6+
### Usage
7+
8+
```typescript
9+
import memoize from '@github/memoize'
10+
11+
const fn = memoize(function doExpensiveStuff() {
12+
// Here's where you do expensive stuff!
13+
})
14+
15+
const other = memoize(function doExpensiveStuff() {}, {
16+
cache: new Map(), // pass your own cache implementation
17+
hash: JSON.stringify // pass your own hashing implementation
18+
})
19+
```
20+
21+
#### Options:
22+
23+
- `hash?: (...args: A) => unknown`
24+
Provides a single value to use as the Key for the memoization.
25+
Defaults to `JSON.stringify` (ish).
26+
- `cache?: Map<unknown, R>`
27+
The Cache implementation to provide. Must be a Map or Map-alike. Defaults to a Map.
28+
Useful for replacing the cache with an LRU cache or similar.
29+
30+
### TypeScript Decorators Support!
31+
32+
This package also includes a decorator module which can be used to provide [TypeScript Decorator](https://www.typescriptlang.org/docs/handbook/decorators.html#decorators) annotations to functions.
33+
34+
Here's an example, showing what you need to do:
35+
36+
```typescript
37+
import memoize from '@github/memoize/decorator'
38+
// ^ note: add `/decorator` to the import to get decorators
39+
40+
class MyClass {
41+
@memoize() // Memoize the method below
42+
doThings() {
43+
}
44+
}
45+
46+
const cache = new Map()
47+
class MyClass {
48+
@memoize({ cache }) // Pass options just like the memoize function
49+
doThings() {
50+
}
51+
}
52+
```
53+
54+
### Why not just use package X?
55+
56+
Many memoize implementations exist. This one provides all of the utility we need at [GitHub](https://github.com/github) and nothing more. We've used a few various implementations in the past, here are some good ones:
57+
58+
- [memoize](https://www.npmjs.com/package/memoize)
59+
- [mem](https://www.npmjs.com/package/mem)
60+
- [lodash.memoize](https://www.npmjs.com/package/lodash.memoize)

decorator.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import memo from './index.js'
2+
import type {MemoizeOptions, MemoizableFunction} from './index.js'
3+
4+
export default function memoize<A extends unknown[], R, T>(memoizeOptions: MemoizeOptions<A, R> = {}) {
5+
return (target: T, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => {
6+
descriptor.value = memo(descriptor.value as MemoizableFunction<A, R, T>, memoizeOptions)
7+
Object.defineProperty(target, propertyKey, descriptor)
8+
}
9+
}

index.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export interface MemoizeOptions<A extends unknown[], R> {
2+
/**
3+
* Provides a single value to use as the Key for the memoization.
4+
* Defaults to `JSON.stringify` (ish).
5+
*/
6+
hash?: (...args: A) => unknown
7+
8+
/**
9+
* The Cache implementation to provide. Must be a Map or Map-alike.
10+
* Defaults to a Map. Useful for replacing the cache with an LRU cache or similar.
11+
*/
12+
cache?: Map<unknown, R>
13+
}
14+
15+
export type MemoizableFunction<A extends unknown[], R extends unknown, T extends unknown> = (this: T, ...args: A) => R
16+
17+
export function defaultHash(...args: unknown[]): string {
18+
// JSON.stringify ellides `undefined` and function values by default. We do not want that.
19+
return JSON.stringify(args, (_: unknown, v: unknown) => (typeof v === 'object' ? v : String(v)))
20+
}
21+
22+
export default function memoize<A extends unknown[], R extends unknown, T extends unknown>(
23+
fn: MemoizableFunction<A, R, T>,
24+
opts: MemoizeOptions<A, R> = {}
25+
): MemoizableFunction<A, R, T> {
26+
const {hash = defaultHash, cache = new Map()} = opts
27+
return function (this: T, ...args: A) {
28+
const id = hash.apply(this, args)
29+
if (cache.has(id)) return cache.get(id)
30+
const result = fn.apply(this, args)
31+
if (result instanceof Promise) {
32+
// eslint-disable-next-line github/no-then
33+
result.catch(error => {
34+
cache.delete(id)
35+
throw error
36+
})
37+
}
38+
cache.set(id, result)
39+
return result
40+
}
41+
}

0 commit comments

Comments
 (0)